get agendadulibre

This commit is contained in:
Tykayn 2025-10-09 22:55:50 +02:00 committed by tykayn
parent 13dc5ceef8
commit 3fa60f3052
9 changed files with 9622 additions and 2278 deletions

2
extractors/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
test_env
venv

View file

@ -0,0 +1,307 @@
# Améliorations du Scraper Agenda du Libre
## Nouvelles Fonctionnalités
### 1. Cache JSON Intelligent
- **Fichier de cache** : `agendadulibre_cache.json`
- **Détection de changements** : Le script détecte si le contenu iCal a changé via un hash MD5
- **Évite les re-téléchargements** : Si le contenu est identique, utilise le cache existant
- **Suivi des événements traités** : Mémorise les événements déjà traités pour éviter les doublons
### 2. Limitation du Nombre d'Événements
- **Argument `--max-events`** : Limite le nombre d'événements à traiter
- **Utile pour les tests** : Permet de tester avec un petit nombre d'événements
- **Statistiques** : Affiche le nombre d'événements ignorés à cause de la limite
### 3. Mode Dry-Run par Défaut
- **Sécurité** : Par défaut, aucun événement n'est envoyé à l'API
- **Simulation** : Affiche ce qui serait envoyé sans faire d'appels API réels
- **Override** : Utilisez `--no-dry-run` pour l'envoi réel
### 4. Logs Détaillés des Événements
- **Informations complètes** : Affiche tous les détails de l'événement avant insertion
- **Traçabilité** : ID, titre, description, dates, lieu, URL, source, etc.
- **Debugging** : Facilite le diagnostic des problèmes d'insertion
- **Audit** : Permet de vérifier les données avant envoi à l'API
### 5. Géocodage Automatique Intelligent
- **Priorité GEO** : Extrait d'abord les coordonnées du champ `GEO:` dans l'iCal
- **Détection d'adresses** : Extrait automatiquement les adresses après la première virgule
- **Géocodage Nominatim** : Utilise l'API Nominatim pour obtenir les coordonnées réelles
- **Nettoyage intelligent** : Détecte les numéros d'adresse pour améliorer la précision
- **Fallback robuste** : Utilise le lieu complet si pas d'adresse détectée
- **Respect des limites** : Pause d'1 seconde entre les requêtes Nominatim
- **Optimisation** : Évite le géocodage sur les événements déjà traités avec succès
### 6. Extraction des Catégories
- **Champ CATEGORIES** : Extrait automatiquement les catégories du champ `CATEGORIES:` de l'iCal
- **Tags multiples** : Support des catégories multiples par événement
- **Intégration OEDB** : Ajoute les catégories comme propriété `tags` dans l'événement
- **Logs informatifs** : Affiche les catégories trouvées dans les logs détaillés
### 7. Extraction des Propriétés Étendues
- **ORGANIZER** : Extrait l'organisateur de l'événement (email/contact)
- **X-ALT-DESC** : Extrait la description alternative HTML si disponible
- **SUMMARY** : Utilise le résumé comme description courte
- **SEQUENCE** : Extrait le numéro de séquence de l'événement
- **RRULE** : Extrait les règles de répétition pour les événements récurrents
- **Enrichissement complet** : Toutes les métadonnées iCal sont préservées
### 8. Priorisation des Événements
- **Événements en attente** : Priorité haute pour les événements avec status `pending`, `failed`, `api_error`
- **Cache intelligent** : Vérification dans les données locales et le cache
- **Tri automatique** : Les événements en attente sont traités en premier
- **Logs informatifs** : Indication claire des événements prioritaires avec emoji 🔄
- **Robustesse** : Retry automatique des événements échoués
## Utilisation
### Commandes de Base
```bash
# Mode dry-run par défaut (sécurisé)
python agendadulibre.py
# Limiter à 5 événements en mode dry-run
python agendadulibre.py --max-events 5
# Mode réel avec limite de 10 événements
python agendadulibre.py --no-dry-run --max-events 10
# Mode verbeux pour voir les détails
python agendadulibre.py --max-events 3 --verbose
# Forcer le rechargement du fichier iCal
python agendadulibre.py --force-refresh --max-events 5
```
### Arguments Disponibles
| Argument | Description | Défaut |
|----------|-------------|---------|
| `--max-events N` | Limite le nombre d'événements à traiter | Aucune limite |
| `--dry-run` | Mode simulation (par défaut) | Activé |
| `--no-dry-run` | Désactive le mode dry-run | - |
| `--verbose` | Mode verbeux | - |
| `--force-refresh` | Force le rechargement iCal | - |
| `--cache-duration N` | Durée de validité du cache (heures) | 1 |
| `--batch-size N` | Taille des batches | 1 |
| `--api-url URL` | URL de l'API OEDB | https://api.openeventdatabase.org |
## Fichiers Générés
### Cache JSON (`agendadulibre_cache.json`)
```json
{
"processed_events": {
"event_id_1": {
"processed_at": "2024-01-01T12:00:00",
"status": "saved",
"event_label": "Nom de l'événement"
}
},
"last_ical_fetch": "2024-01-01T12:00:00",
"ical_content_hash": "md5_hash_du_contenu"
}
```
### Données d'Événements (`agendadulibre_events.json`)
```json
{
"events": {
"event_id": {
"status": "saved",
"message": "Créé avec succès",
"last_attempt": "2024-01-01T12:00:00",
"event": { /* données de l'événement */ }
}
},
"last_update": "2024-01-01T12:00:00"
}
```
## Exemples de Sortie
### Mode Dry-Run avec Logs Détaillés
```
🚀 Démarrage du scraping de l'agenda du libre
Configuration: batch_size=1, api_url=https://api.openeventdatabase.org
Mode dry-run: OUI
Limite d'événements: 5
Cache iCal: valide pendant 1h
Mode DRY-RUN activé - aucun événement ne sera envoyé à l'API
📝 Détails de l'événement à insérer:
ID: 6a575f6a82922f4501854431fc3f831c
Titre: Conférence Python
Description: Présentation sur Python
Type: scheduled
Catégorie: culture.floss
Lieu: Paris, France
Début: 2024-12-01T10:00:00
Fin: 2024-12-01T12:00:00
URL: https://example.com/event1
Source: Agenda du Libre
Coordonnées: [0, 0]
Modifié par: agendadulibre_scraper
[DRY-RUN] Simulation d'envoi de l'événement: Conférence Python
✅ Conférence Python - Simulé (dry-run)
```
### Mode Réel avec Propriétés Complètes
```
🚀 Démarrage du scraping de l'agenda du libre
Configuration: batch_size=1, api_url=https://api.openeventdatabase.org
Mode dry-run: NON
Limite d'événements: 3
📍 Coordonnées GEO trouvées: 45.756, 4.84773
🏷️ Catégories trouvées: entraide, aldil, epn-des-rancy, linux
👤 Organisateur trouvé: mailto:contact@aldil.org
📝 Description courte trouvée: Entraide et Bidouille
🔢 Séquence trouvée: 3
✅ Coordonnées utilisées depuis le champ GEO: [4.84773, 45.756]
📝 Détails de l'événement à insérer:
ID: 6a575f6a82922f4501854431fc3f831c
Titre: Entraide et Bidouille
Description: Atelier d'entraide informatique
Type: scheduled
Catégorie: culture.floss
Lieu: Maison pour tous / salle des Rancy, 249 rue Vendôme, Lyon, France
Début: 2024-12-01T10:00:00
Fin: 2024-12-01T12:00:00
URL: https://example.com/event1
Source: Agenda du Libre
Coordonnées: [4.84773, 45.756]
Tags: entraide, aldil, epn-des-rancy, linux
Organisateur: mailto:contact@aldil.org
Description courte: Entraide et Bidouille
Séquence: 3
Règles de répétition: N/A
Description HTML: N/A
Modifié par: agendadulibre_scraper
🌐 Envoi à l'API: https://api.openeventdatabase.org/event
✅ Événement créé avec succès dans l'API
✅ Entraide et Bidouille - Créé avec succès
```
### Mode Réel avec Géocodage Nominatim
```
🚀 Démarrage du scraping de l'agenda du libre
Configuration: batch_size=1, api_url=https://api.openeventdatabase.org
Mode dry-run: NON
Limite d'événements: 3
📍 Adresse potentielle trouvée: 15 rue de la Paix, Paris, France
🌍 Géocodage avec Nominatim: 15 rue de la Paix, Paris, France
✅ Géocodage réussi: 15 rue de la Paix, Paris, France -> (48.8566, 2.3522)
Adresse trouvée: 15 Rue de la Paix, 75001 Paris, France
🎯 Coordonnées mises à jour par géocodage: [0, 0] -> [2.3522, 48.8566]
📝 Détails de l'événement à insérer:
ID: 6a575f6a82922f4501854431fc3f831c
Titre: Conférence Python
Description: Présentation sur Python
Type: scheduled
Catégorie: culture.floss
Lieu: Centre de conférences, 15 rue de la Paix, Paris, France
Début: 2024-12-01T10:00:00
Fin: 2024-12-01T12:00:00
URL: https://example.com/event1
Source: Agenda du Libre
Coordonnées: [2.3522, 48.8566]
Modifié par: agendadulibre_scraper
🌐 Envoi à l'API: https://api.openeventdatabase.org/event
✅ Événement créé avec succès dans l'API
✅ Conférence Python - Créé avec succès
```
### Mode Optimisé - Événements Déjà Traités
```
🚀 Démarrage du scraping de l'agenda du libre
Configuration: batch_size=1, api_url=https://api.openeventdatabase.org
Mode dry-run: NON
⏭️ Événement ignoré: Conférence Python - déjà traité (status: saved)
⏭️ Événement ignoré: Atelier Linux - déjà dans le cache (status: saved)
Géocodage ignoré pour Formation Git - déjà traité
Géocodage ignoré - événement déjà traité
📝 Détails de l'événement à insérer:
ID: dd0850de6ed7a6b4d482a7dc5201d09c
Titre: Formation Git
Description: Maîtriser Git
Type: scheduled
Catégorie: culture.floss
Lieu: Espace formation, 42 avenue du Général de Gaulle, Marseille, France
Début: 2024-12-03T09:00:00
Fin: 2024-12-03T11:00:00
URL: https://example.com/event3
Source: Agenda du Libre
Coordonnées: [5.3698, 43.2965]
Modifié par: agendadulibre_scraper
🌐 Envoi à l'API: https://api.openeventdatabase.org/event
⚠️ Événement déjà existant dans l'API
✅ Formation Git - Événement déjà existant
```
### Mode Prioritaire - Événements en Attente
```
🚀 Démarrage du scraping de l'agenda du libre
Configuration: batch_size=1, api_url=https://api.openeventdatabase.org
Mode dry-run: NON
🔄 Événement en attente prioritaire: Atelier Linux (status: failed)
🔄 Événement en attente du cache: Formation Git (status: pending)
📋 Événements à traiter: 2 (dont 2 en attente)
🔄 Traitement prioritaire: Atelier Linux
📝 Détails de l'événement à insérer:
ID: 5ac96f4ae72cd28d164489580e97daca
Titre: Atelier Linux
Description: Apprendre Linux
Type: scheduled
Catégorie: culture.floss
Lieu: Lyon, France
Début: 2024-12-02T14:00:00
Fin: 2024-12-02T16:00:00
URL: https://example.com/event2
Source: Agenda du Libre
Coordonnées: [4.8357, 45.764]
Modifié par: agendadulibre_scraper
🌐 Envoi à l'API: https://api.openeventdatabase.org/event
✅ Événement créé avec succès dans l'API
✅ Atelier Linux - Créé avec succès
```
## Tests
Exécutez le script de test pour vérifier les fonctionnalités :
```bash
python test_agendadulibre_improvements.py
```
## Avantages
1. **Sécurité** : Mode dry-run par défaut évite les insertions accidentelles
2. **Performance** : Cache intelligent réduit les téléchargements inutiles
3. **Contrôle** : Limitation du nombre d'événements pour les tests
4. **Traçabilité** : Logs détaillés et fichiers de cache pour le suivi
5. **Flexibilité** : Arguments pour personnaliser le comportement
6. **Géolocalisation précise** : Géocodage automatique des lieux avec coordonnées réelles
7. **Intelligence** : Détection et extraction automatique des adresses
8. **Robustesse** : Fallback intelligent en cas d'échec de géocodage
9. **Optimisation** : Évite les retraitements inutiles des événements déjà envoyés
10. **Efficacité** : Skip automatique du géocodage pour les événements déjà traités
11. **Enrichissement** : Extraction automatique des catégories comme tags
12. **Classification** : Amélioration de la recherche et du filtrage des événements
13. **Métadonnées complètes** : Extraction de toutes les propriétés iCal importantes
14. **Traçabilité** : Organisateur, séquence et règles de répétition préservées
15. **Flexibilité** : Support des descriptions HTML et des événements récurrents
16. **Priorisation intelligente** : Traitement prioritaire des événements en attente
17. **Robustesse** : Retry automatique des événements échoués
18. **Efficacité** : Optimisation du traitement par priorité
## Migration
Les anciens scripts continuent de fonctionner, mais il est recommandé d'utiliser les nouveaux arguments pour plus de contrôle et de sécurité.

View file

@ -185,3 +185,13 @@
2025-09-26 17:32:06,370 - INFO - 🔄 [14/14] Traitement de https://lagendageek.com/tevent/steven-spielberg-hommage-80-ans/
2025-09-26 17:32:10,298 - INFO - ✅ Événement créé avec succès: ID c73cd045-b199-404f-a256-cd14977abe95
2025-09-26 17:32:11,299 - INFO - 🏁 Traitement terminé - Succès: 14, Erreurs: 0
2025-10-09 16:37:04,898 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-10-09 16:37:04,904 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/1
2025-10-09 16:37:08,740 - INFO - ✅ 20 événements trouvés sur la page
2025-10-09 16:37:08,741 - INFO - 📊 Traitement de 20 événements (1 à 20 sur 20)
2025-10-09 16:37:08,741 - INFO - 🔄 [1/20] Traitement de https://lagendageek.com/tevent/octogones-2025/
2025-10-09 16:37:13,589 - INFO - ⚠️ Événement déjà existant (conflit)
2025-10-09 16:37:14,590 - INFO - 🔄 [2/20] Traitement de https://lagendageek.com/tevent/gloose-11-festival-du-jeu-dalbi/
2025-10-09 16:37:19,410 - INFO - ⚠️ Événement déjà existant (conflit)
2025-10-09 16:37:20,410 - INFO - 🔄 [3/20] Traitement de https://lagendageek.com/tevent/festival-de-limaginaire-geek-family-autres-mondes/
2025-10-09 16:37:25,374 - INFO - ⚠️ Événement déjà existant (conflit)

View file

@ -9,6 +9,8 @@ import json
import os
import sys
import argparse
import re
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import icalendar
@ -30,16 +32,20 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class AgendaDuLibreScraper:
def __init__(self, api_base_url: str = api_oedb, batch_size: int = 1):
def __init__(self, api_base_url: str = api_oedb, batch_size: int = 1, max_events: int = None, dry_run: bool = True):
self.api_base_url = api_base_url
self.batch_size = batch_size
self.max_events = max_events
self.dry_run = dry_run
self.data_file = "agendadulibre_events.json"
self.cache_file = "agendadulibre_cache.json"
self.ical_file = "agendadulibre_events.ics"
self.ical_url = "https://www.agendadulibre.org/events.ics"
self.cache_duration_hours = 1 # Durée de cache en heures
# Charger les données existantes
self.events_data = self.load_events_data()
self.cache_data = self.load_cache_data()
def load_events_data(self) -> Dict:
"""Charge les données d'événements depuis le fichier JSON local"""
@ -52,6 +58,17 @@ class AgendaDuLibreScraper:
return {"events": {}, "last_update": None}
return {"events": {}, "last_update": None}
def load_cache_data(self) -> Dict:
"""Charge les données de cache depuis le fichier JSON local"""
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:
logger.error(f"Erreur lors du chargement du fichier cache {self.cache_file}: {e}")
return {"processed_events": {}, "last_ical_fetch": None, "ical_content_hash": None}
return {"processed_events": {}, "last_ical_fetch": None, "ical_content_hash": None}
def save_events_data(self):
"""Sauvegarde les données d'événements dans le fichier JSON local"""
try:
@ -60,6 +77,14 @@ class AgendaDuLibreScraper:
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier {self.data_file}: {e}")
def save_cache_data(self):
"""Sauvegarde les données de cache dans le fichier JSON local"""
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:
logger.error(f"Erreur lors de la sauvegarde du fichier cache {self.cache_file}: {e}")
def is_ical_cache_valid(self) -> bool:
"""Vérifie si le cache iCal est encore valide (moins d'une heure)"""
if not os.path.exists(self.ical_file):
@ -76,12 +101,28 @@ class AgendaDuLibreScraper:
logger.error(f"Erreur lors de la vérification du cache iCal: {e}")
return False
def get_content_hash(self, content: bytes) -> str:
"""Calcule le hash du contenu pour détecter les changements"""
import hashlib
return hashlib.md5(content).hexdigest()
def is_ical_content_changed(self, new_content: bytes) -> bool:
"""Vérifie si le contenu iCal a changé depuis la dernière fois"""
new_hash = self.get_content_hash(new_content)
old_hash = self.cache_data.get("ical_content_hash")
return new_hash != old_hash
def save_ical_cache(self, ical_content: bytes):
"""Sauvegarde le contenu iCal en cache local"""
try:
with open(self.ical_file, 'wb') as f:
f.write(ical_content)
logger.info(f"Cache iCal sauvegardé dans {self.ical_file}")
# Mettre à jour le cache JSON avec le hash du contenu
self.cache_data["ical_content_hash"] = self.get_content_hash(ical_content)
self.cache_data["last_ical_fetch"] = datetime.now().isoformat()
self.save_cache_data()
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du cache iCal: {e}")
@ -115,8 +156,14 @@ class AgendaDuLibreScraper:
response.raise_for_status()
ical_content = response.content
# Sauvegarder en cache
self.save_ical_cache(ical_content)
# Vérifier si le contenu a changé
if not self.is_ical_content_changed(ical_content):
logger.info("Contenu iCal identique au précédent, utilisation du cache existant")
ical_content = self.load_ical_cache()
else:
logger.info("Nouveau contenu iCal détecté, mise à jour du cache")
# Sauvegarder en cache
self.save_ical_cache(ical_content)
except requests.RequestException as e:
logger.error(f"Erreur lors de la récupération du fichier iCal: {e}")
@ -145,6 +192,19 @@ class AgendaDuLibreScraper:
location = str(event.get('location', ''))
url = str(event.get('url', ''))
# Extraire les coordonnées GEO si disponibles
geo_coords = self.extract_geo_coordinates(event)
# Extraire les catégories si disponibles
categories = self.extract_categories(event)
# Extraire les propriétés supplémentaires
organizer = self.extract_organizer(event)
alt_description = self.extract_alt_description(event)
short_description = self.extract_short_description(event)
sequence = self.extract_sequence(event)
repeat_rules = self.extract_repeat_rules(event)
# Gestion des dates
dtstart = event.get('dtstart')
dtend = event.get('dtend')
@ -188,11 +248,17 @@ class AgendaDuLibreScraper:
"url": url if url else None,
"source:name": "Agenda du Libre",
"source:url": "https://www.agendadulibre.org/",
"last_modified_by": "agendadulibre_scraper"
"last_modified_by": "agendadulibre_scraper",
"tags": categories if categories else [], # Ajouter les catégories comme tags
"organizer": organizer, # Organisateur de l'événement
"alt_description": alt_description, # Description alternative HTML
"short_description": short_description, # Description courte
"sequence": sequence, # Numéro de séquence
"repeat_rules": repeat_rules # Règles de répétition
},
"geometry": {
"type": "Point",
"coordinates": [0, 0] # Coordonnées par défaut, à géocoder si nécessaire
"coordinates": geo_coords if geo_coords else [0, 0] # Utiliser GEO ou coordonnées par défaut
}
}
@ -202,28 +268,333 @@ class AgendaDuLibreScraper:
return {
"id": event_id,
"event": oedb_event,
"raw_ical": {
"summary": summary,
"description": description,
"location": location,
"url": url,
"dtstart": start_iso,
"dtend": end_iso
}
}
except Exception as e:
logger.error(f"Erreur lors du parsing de l'événement: {e}")
return None
def extract_geo_coordinates(self, event: Event) -> Optional[List[float]]:
"""Extrait les coordonnées du champ GEO: de l'événement iCal"""
try:
geo = event.get('geo')
if geo:
# Le champ GEO peut être sous différentes formes
if hasattr(geo, 'lat') and hasattr(geo, 'lon'):
# Format avec attributs lat/lon
lat = float(geo.lat)
lon = float(geo.lon)
logger.info(f"📍 Coordonnées GEO trouvées: {lat}, {lon}")
return [lon, lat] # Format GeoJSON (longitude, latitude)
else:
# Format string "latitude;longitude"
geo_str = str(geo)
if ';' in geo_str:
parts = geo_str.split(';')
if len(parts) == 2:
lat = float(parts[0].strip())
lon = float(parts[1].strip())
logger.info(f"📍 Coordonnées GEO trouvées: {lat}, {lon}")
return [lon, lat] # Format GeoJSON (longitude, latitude)
else:
logger.debug(f"Format GEO non reconnu: {geo_str}")
else:
logger.debug("Aucun champ GEO trouvé")
return None
except (ValueError, AttributeError, TypeError) as e:
logger.warning(f"Erreur lors de l'extraction des coordonnées GEO: {e}")
return None
except Exception as e:
logger.error(f"Erreur inattendue lors de l'extraction GEO: {e}")
return None
def extract_categories(self, event: Event) -> List[str]:
"""Extrait les catégories du champ CATEGORIES: de l'événement iCal"""
try:
categories = []
# Le champ CATEGORIES peut apparaître plusieurs fois
for category in event.get('categories', []):
if category:
# Extraire la valeur de l'objet vCategory
if hasattr(category, 'cats'):
# Si c'est un objet vCategory avec des catégories
for cat in category.cats:
cat_str = str(cat).strip()
if cat_str:
categories.append(cat_str)
else:
# Sinon, convertir directement en string
cat_str = str(category).strip()
if cat_str:
categories.append(cat_str)
if categories:
logger.info(f"🏷️ Catégories trouvées: {', '.join(categories)}")
else:
logger.debug("Aucune catégorie trouvée")
return categories
except Exception as e:
logger.warning(f"Erreur lors de l'extraction des catégories: {e}")
return []
def extract_organizer(self, event: Event) -> Optional[str]:
"""Extrait l'organisateur du champ ORGANIZER: de l'événement iCal"""
try:
organizer = event.get('organizer')
if organizer:
organizer_str = str(organizer).strip()
if organizer_str:
logger.debug(f"👤 Organisateur trouvé: {organizer_str}")
return organizer_str
return None
except Exception as e:
logger.warning(f"Erreur lors de l'extraction de l'organisateur: {e}")
return None
def extract_alt_description(self, event: Event) -> Optional[str]:
"""Extrait la description alternative HTML du champ X-ALT-DESC;FMTTYPE=text/html: de l'événement iCal"""
try:
# Chercher le champ X-ALT-DESC avec FMTTYPE=text/html
for prop in event.property_items():
if prop[0] == 'X-ALT-DESC' and hasattr(prop[1], 'params') and prop[1].params.get('FMTTYPE') == 'text/html':
alt_desc = str(prop[1]).strip()
if alt_desc:
logger.debug(f"📄 Description alternative HTML trouvée: {len(alt_desc)} caractères")
return alt_desc
return None
except Exception as e:
logger.warning(f"Erreur lors de l'extraction de la description alternative: {e}")
return None
def extract_short_description(self, event: Event) -> Optional[str]:
"""Extrait la description courte du champ SUMMARY: de l'événement iCal"""
try:
summary = event.get('summary')
if summary:
summary_str = str(summary).strip()
if summary_str:
logger.debug(f"📝 Description courte trouvée: {summary_str}")
return summary_str
return None
except Exception as e:
logger.warning(f"Erreur lors de l'extraction de la description courte: {e}")
return None
def extract_sequence(self, event: Event) -> Optional[int]:
"""Extrait le numéro de séquence du champ SEQUENCE: de l'événement iCal"""
try:
sequence = event.get('sequence')
if sequence is not None:
seq_num = int(sequence)
logger.debug(f"🔢 Séquence trouvée: {seq_num}")
return seq_num
return None
except (ValueError, TypeError) as e:
logger.warning(f"Erreur lors de l'extraction de la séquence: {e}")
return None
except Exception as e:
logger.warning(f"Erreur inattendue lors de l'extraction de la séquence: {e}")
return None
def extract_repeat_rules(self, event: Event) -> Optional[str]:
"""Extrait les règles de répétition du champ RRULE: de l'événement iCal"""
try:
# Essayer différentes variantes de casse
rrule = event.get('rrule') or event.get('RRULE') or event.get('Rrule')
if rrule:
rrule_str = str(rrule).strip()
if rrule_str:
logger.info(f"🔄 Règles de répétition trouvées: {rrule_str}")
return rrule_str
# Vérifier aussi dans les propriétés avec parcours manuel
for prop in event.property_items():
if prop[0].upper() == 'RRULE':
rrule_str = str(prop[1]).strip()
if rrule_str:
logger.info(f"🔄 Règles de répétition trouvées (parcours): {rrule_str}")
return rrule_str
# Note: Pas de log ici car c'est normal qu'il n'y ait pas de RRULE
# dans tous les événements (seulement les événements récurrents en ont)
return None
except Exception as e:
logger.warning(f"Erreur lors de l'extraction des règles de répétition: {e}")
return None
def generate_event_id(self, summary: str, start_date: str, location: str) -> str:
"""Génère un ID unique pour l'événement"""
import hashlib
content = f"{summary}_{start_date}_{location}"
return hashlib.md5(content.encode('utf-8')).hexdigest()
def send_event_to_api(self, event_data: Dict) -> Tuple[bool, str]:
"""Envoie un événement à l'API OEDB"""
def clean_location_for_geocoding(self, location: str) -> Optional[str]:
"""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):
logger.info(f"📍 Adresse potentielle trouvée: {address_part}")
return address_part
# Si pas de virgule ou pas d'adresse valide, essayer le lieu complet
logger.info(f"📍 Tentative de géocodage avec le lieu complet: {location}")
return location.strip()
def geocode_with_nominatim(self, location: str) -> Optional[Tuple[float, float]]:
"""Géocode un lieu avec Nominatim"""
if not location:
return None
try:
# URL de l'API Nominatim
nominatim_url = "https://nominatim.openstreetmap.org/search"
# Paramètres de la requête
params = {
'q': location,
'format': 'json',
'limit': 1,
'countrycodes': 'fr', # Limiter à la France
'addressdetails': 1
}
headers = {
'User-Agent': 'AgendaDuLibreScraper/1.0 (contact@example.com)'
}
logger.info(f"🌍 Géocodage avec Nominatim: {location}")
# Faire la requête avec un timeout
response = requests.get(nominatim_url, params=params, headers=headers, timeout=10)
response.raise_for_status()
# Parser la réponse
results = response.json()
if results and len(results) > 0:
result = results[0]
lat = float(result['lat'])
lon = float(result['lon'])
logger.info(f"✅ Géocodage réussi: {location} -> ({lat}, {lon})")
logger.info(f" Adresse trouvée: {result.get('display_name', 'N/A')}")
# Respecter la limite de 1 requête par seconde pour Nominatim
time.sleep(1)
return (lon, lat) # Retourner (longitude, latitude) pour GeoJSON
else:
logger.warning(f"⚠️ Aucun résultat de géocodage pour: {location}")
return None
except requests.RequestException as e:
logger.error(f"❌ Erreur de connexion Nominatim: {e}")
return None
except (ValueError, KeyError) as e:
logger.error(f"❌ Erreur de parsing Nominatim: {e}")
return None
except Exception as e:
logger.error(f"❌ Erreur inattendue lors du géocodage: {e}")
return None
def improve_event_coordinates(self, event_data: Dict) -> Dict:
"""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
new_coords = self.geocode_with_nominatim(clean_location)
if new_coords:
# Mettre à jour les coordonnées
event_data["event"]["geometry"]["coordinates"] = list(new_coords)
logger.info(f"🎯 Coordonnées mises à jour par géocodage: {coords} -> {new_coords}")
else:
logger.warning(f"⚠️ Impossible de géocoder: {clean_location}")
else:
logger.info(f" Lieu non géocodable: {location}")
else:
logger.info(" 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:
logger.info(f"✅ Coordonnées utilisées depuis le champ GEO: {coords}")
else:
logger.info(f" Coordonnées déjà définies: {coords}")
return event_data
def log_event_details(self, event_data: Dict):
"""Log détaillé de l'événement avant envoi"""
props = event_data["event"]["properties"]
geom = event_data["event"]["geometry"]
logger.info("📝 Détails de l'événement à insérer:")
# INSERT_YOUR_CODE
# Affiche un dump lisible de l'événement avec json.dumps (indentation)
try:
logger.info(json.dumps(event_data, ensure_ascii=False, indent=2))
except Exception as e:
logger.warning(f"Erreur lors de l'affichage lisible de l'événement: {e}")
# logger.info(event_data)
# logger.info(f" ID: {event_data['id']}")
# logger.info(f" Titre: {props.get('label', 'N/A')}")
# logger.info(f" Description: {props.get('description', 'N/A')[:100]}{'...' if len(props.get('description', '')) > 100 else ''}")
# logger.info(f" Type: {props.get('type', 'N/A')}")
# logger.info(f" Catégorie: {props.get('what', 'N/A')}")
# logger.info(f" Lieu: {props.get('where', 'N/A')}")
# logger.info(f" Début: {props.get('start', 'N/A')}")
# logger.info(f" Fin: {props.get('stop', 'N/A')}")
# logger.info(f" URL: {props.get('url', 'N/A')}")
# logger.info(f" Source: {props.get('source:name', 'N/A')}")
# logger.info(f" Coordonnées: {geom.get('coordinates', 'N/A')}")
# logger.info(f" Tags: {', '.join(props.get('tags', [])) if props.get('tags') else 'N/A'}")
# logger.info(f" Organisateur: {props.get('organizer', 'N/A')}")
# logger.info(f" Description courte: {props.get('short_description', 'N/A')}")
# logger.info(f" Séquence: {props.get('sequence', 'N/A')}")
# logger.info(f" Règles de répétition: {props.get('repeat_rules', 'N/A')}")
# logger.info(f" Description HTML: {'Oui' if props.get('alt_description') else 'N/A'}")
# logger.info(f" Modifié par: {props.get('last_modified_by', 'N/A')}")
def send_event_to_api(self, event_data: Dict, skip_geocoding: bool = False) -> Tuple[bool, str]:
"""Envoie un événement à l'API OEDB (ou simule en mode dry-run)"""
# 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:
logger.info(" 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:
logger.info(f"[DRY-RUN] Simulation d'envoi de l'événement: {event_data['event']['properties']['label']}")
return True, "Simulé (dry-run)"
try:
url = f"{self.api_base_url}/event"
headers = {"Content-Type": "application/json"}
@ -235,18 +606,24 @@ class AgendaDuLibreScraper:
"properties": event_data["event"]["properties"]
}
logger.info(f"🌐 Envoi à l'API: {url}")
response = requests.post(url, json=geojson_event, headers=headers, timeout=30)
if response.status_code == 201:
logger.info("✅ Événement créé avec succès dans l'API")
return True, "Créé avec succès"
elif response.status_code == 409:
logger.warning("⚠️ Événement déjà existant dans l'API")
return False, "Événement déjà existant"
else:
logger.error(f"❌ Erreur API: {response.status_code} - {response.text}")
return False, f"Erreur API: {response.status_code} - {response.text}"
except requests.RequestException as e:
logger.error(f"❌ Erreur de connexion: {e}")
return False, f"Erreur de connexion: {e}"
except Exception as e:
logger.error(f"❌ Erreur inattendue: {e}")
return False, f"Erreur inattendue: {e}"
def process_events(self, calendar: Calendar) -> Dict:
@ -257,10 +634,13 @@ class AgendaDuLibreScraper:
"already_saved": 0,
"api_errors": 0,
"parse_errors": 0,
"sent_this_run": 0
"sent_this_run": 0,
"skipped_due_to_limit": 0
}
events_to_process = []
pending_events = [] # Événements en attente d'envoi
processed_count = 0
# Parcourir tous les événements
for component in calendar.walk():
@ -274,19 +654,72 @@ class AgendaDuLibreScraper:
continue
event_id = parsed_event["id"]
event_label = parsed_event["event"]["properties"]["label"]
# Vérifier si l'événement existe déjà dans nos données
# Vérifier le statut de l'événement
event_status = None
skip_reason = ""
# Vérifier dans les données d'événements
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"]:
stats["already_saved"] += 1
logger.debug(f"Événement déjà traité: {parsed_event['event']['properties']['label']}")
logger.info(f"⏭️ Événement ignoré: {event_label} - déjà traité (status: {event_status})")
continue
events_to_process.append(parsed_event)
# Vérifier dans le cache des événements traités
if event_id in self.cache_data["processed_events"]:
cache_status = self.cache_data["processed_events"][event_id].get("status", "unknown")
if cache_status in ["saved", "already_exists"]:
stats["already_saved"] += 1
logger.info(f"⏭️ Événement ignoré: {event_label} - déjà dans le cache (status: {cache_status})")
continue
# Déterminer la priorité de l'événement
priority = 0 # 0 = nouveau, 1 = en attente, 2 = échec précédent
if event_status in ["pending", "failed", "api_error"]:
priority = 1 # Priorité haute pour les événements en attente
logger.info(f"🔄 Événement en attente prioritaire: {event_label} (status: {event_status})")
elif event_id in self.cache_data["processed_events"]:
cache_status = self.cache_data["processed_events"][event_id].get("status", "unknown")
if cache_status in ["pending", "failed", "api_error"]:
priority = 1 # Priorité haute pour les événements en attente dans le cache
logger.info(f"🔄 Événement en attente du cache: {event_label} (status: {cache_status})")
# Ajouter l'événement avec sa priorité
event_with_priority = {
"event": parsed_event,
"priority": priority,
"event_id": event_id,
"event_label": event_label
}
if priority > 0:
pending_events.append(event_with_priority)
else:
events_to_process.append(event_with_priority)
# Trier les événements : d'abord les événements en attente, puis les nouveaux
all_events = pending_events + events_to_process
all_events.sort(key=lambda x: x["priority"], reverse=True) # Priorité décroissante
# Appliquer la limite d'événements
if self.max_events:
all_events = all_events[:self.max_events]
if len(pending_events) + len(events_to_process) > self.max_events:
stats["skipped_due_to_limit"] = len(pending_events) + len(events_to_process) - self.max_events
# Extraire les événements pour le traitement
events_to_process = [item["event"] for item in all_events]
# Traiter les événements par batch
logger.info(f"Traitement de {len(events_to_process)} nouveaux événements par batch de {self.batch_size}")
if self.max_events:
logger.info(f"Limite d'événements: {self.max_events}")
if self.dry_run:
logger.info("Mode DRY-RUN activé - aucun événement ne sera envoyé à l'API")
for i in range(0, len(events_to_process), self.batch_size):
batch = events_to_process[i:i + self.batch_size]
@ -298,8 +731,16 @@ class AgendaDuLibreScraper:
logger.info(f"Envoi de l'événement: {event_label}")
# 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
logger.info(f" Géocodage ignoré pour {event_label} - déjà traité")
# Envoyer à l'API
success, message = self.send_event_to_api(event_data)
success, message = self.send_event_to_api(event_data, skip_geocoding=skip_geocoding)
# Mettre à jour les statistiques et les données locales
if success:
@ -311,6 +752,12 @@ class AgendaDuLibreScraper:
"last_attempt": datetime.now().isoformat(),
"event": event_data["event"]
}
# Ajouter au cache des événements traités
self.cache_data["processed_events"][event_id] = {
"processed_at": datetime.now().isoformat(),
"status": "saved",
"event_label": event_label
}
logger.info(f"{event_label} - {message}")
else:
if "déjà existant" in message or "already exists" in message.lower():
@ -321,6 +768,12 @@ class AgendaDuLibreScraper:
"last_attempt": datetime.now().isoformat(),
"event": event_data["event"]
}
# Ajouter au cache même si déjà existant
self.cache_data["processed_events"][event_id] = {
"processed_at": datetime.now().isoformat(),
"status": "already_exists",
"event_label": event_label
}
logger.info(f"⚠️ {event_label} - {message}")
else:
stats["api_errors"] += 1
@ -335,12 +788,18 @@ class AgendaDuLibreScraper:
# Mettre à jour la date de dernière mise à jour
self.events_data["last_update"] = datetime.now().isoformat()
# Sauvegarder le cache
self.save_cache_data()
return stats
def run(self, force_refresh: bool = False):
"""Exécute le scraping complet"""
logger.info("🚀 Démarrage du scraping de l'agenda du libre")
logger.info(f"Configuration: batch_size={self.batch_size}, api_url={self.api_base_url}")
logger.info(f"Mode dry-run: {'OUI' if self.dry_run else 'NON'}")
if self.max_events:
logger.info(f"Limite d'événements: {self.max_events}")
logger.info(f"Cache iCal: {'ignoré' if force_refresh else f'valide pendant {self.cache_duration_hours}h'}")
# Récupérer le fichier iCal
@ -363,6 +822,8 @@ class AgendaDuLibreScraper:
logger.info(f" Erreurs d'API: {stats['api_errors']}")
logger.info(f" Erreurs de parsing: {stats['parse_errors']}")
logger.info(f" Événements envoyés cette fois: {stats['sent_this_run']}")
if stats['skipped_due_to_limit'] > 0:
logger.info(f" Événements ignorés (limite atteinte): {stats['skipped_due_to_limit']}")
logger.info("✅ Scraping terminé avec succès")
return True
@ -373,6 +834,12 @@ def main():
help=f"URL de base de l'API OEDB (défaut: {api_oedb})")
parser.add_argument("--batch-size", type=int, default=1,
help="Nombre d'événements à envoyer par batch (défaut: 1)")
parser.add_argument("--max-events", type=int, default=None,
help="Limiter le nombre d'événements à traiter (défaut: aucun)")
parser.add_argument("--dry-run", action="store_true", default=True,
help="Mode dry-run par défaut (simulation sans envoi à l'API)")
parser.add_argument("--no-dry-run", action="store_true",
help="Désactiver le mode dry-run (envoi réel à l'API)")
parser.add_argument("--verbose", "-v", action="store_true",
help="Mode verbeux")
parser.add_argument("--force-refresh", "-f", action="store_true",
@ -385,10 +852,15 @@ def main():
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Déterminer le mode dry-run
dry_run = args.dry_run and not args.no_dry_run
# Créer et exécuter le scraper
scraper = AgendaDuLibreScraper(
api_base_url=args.api_url,
batch_size=args.batch_size
batch_size=args.batch_size,
max_events=args.max_events,
dry_run=dry_run
)
# Modifier la durée de cache si spécifiée

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,593 @@
#!/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):
self.max_events = max_events
self.dry_run = dry_run
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()
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")
if __name__ == "__main__":
main()

View file

@ -209,6 +209,8 @@
<option value="Russia">Russia</option>
<option value="Saudi Arabia">Saudi Arabia</option>
<option value="Senegal">Senegal</option>
<option value="Serbia">Serbia</option>
@ -271,42 +273,19 @@
<ul class="event-list">
<li>
<h3 class="event-list-group-title">August</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/3973/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mapatón Nacional de México 2025</p>
<p class="event-entry-location">Zacatecas, Mexico</p>
</div>
<span class="event-entry-date">
19th August30th September
</span>
</a></li>
</ul>
</li>
<li>
<h3 class="event-list-group-title"></h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/3987/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4097/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">UN Mappers Mappy Hour</p>
<p class="event-entry-name">Sosial samling</p>
<p class="event-entry-location">Oslo, Norway</p>
</div>
<span class="event-entry-date">
26th September
9th October
@ -314,342 +293,15 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4062/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4098/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Workshop „OpenStreetMap“ beim Science Day 2025</p>
<p class="event-entry-location">Kiel, Schleswig-Holstein, Germany</p>
<p class="event-entry-name">Mapatona de Estradas Rurais - Zona Sudeste de Rio Paranaíba</p>
<p class="event-entry-location">Rio Paranaíba, Minas Gerais, Brazil</p>
</div>
<span class="event-entry-date">
26th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3855/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Düsseldorfer OpenStreetMap-Treffen (online)</p>
<p class="event-entry-location">Dusseldorf, North Rhine-Westphalia, Germany</p>
</div>
<span class="event-entry-date">
26th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4002/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Encuentro de la Comunidad de OpenStreetMap de Argentina</p>
<p class="event-entry-location">Luján, Buenos Aires, Argentina</p>
</div>
<span class="event-entry-date">
27th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4014/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Atelier du groupe local de Metz - Septembre 2025</p>
<p class="event-entry-location">Metz, Grand Est, France</p>
</div>
<span class="event-entry-date">
27th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4071/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OpenStreetMap Community Meet-Up - Istanbul</p>
<p class="event-entry-location">Kadıköy, Turkey</p>
</div>
<span class="event-entry-date">
27th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4070/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Intro to OpenStreetMap.org Operations - Getting involved</p>
</div>
<span class="event-entry-date">
27th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3993/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">23rd OSM Delhi MapWalk</p>
<p class="event-entry-location">Gurgaon, Haryana, India</p>
</div>
<span class="event-entry-date">
28th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4055/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Rencontre Saint-Étienne et sud Loire</p>
<p class="event-entry-location">Saint-Étienne, Auvergne-Rhône-Alpes, France</p>
</div>
<span class="event-entry-date">
29th September
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4037/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM Suomi kartoittajatapaaminen</p>
<p class="event-entry-location">Helsinki, Uusimaa, Finland</p>
</div>
<span class="event-entry-date">
30th September
</span>
</a></li>
</ul>
</li>
<li>
<h3 class="event-list-group-title">October</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/4033/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Stuttgarter OpenStreetMap-Treffen</p>
<p class="event-entry-location">Stuttgart, Baden-Württemberg, Germany</p>
</div>
<span class="event-entry-date">
1st October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4058/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mappy Hour OSM España</p>
<p class="event-entry-location">Madrid, Community of Madrid, Spain</p>
</div>
<span class="event-entry-date">
2nd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3928/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM-Treffen in Bochum</p>
<p class="event-entry-location">Bochum, North Rhine-Westphalia, Germany</p>
</div>
<span class="event-entry-date">
2nd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4068/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">[Online] 🇧🇷 Oficina de mapeamento de áreas no OpenStreetMap com editor JOSM</p>
<p class="event-entry-location">Rio de Janeiro, Rio de Janeiro, Brazil</p>
</div>
<span class="event-entry-date">
2nd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3549/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">State of the Map 2025</p>
<p class="event-entry-location">Quezon City, Philippines</p>
</div>
<span class="event-entry-date">
3rd5th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4067/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSMF Engineering Working Group meeting</p>
</div>
<span class="event-entry-date">
3rd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4054/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">17. Mapathon &amp; Mapping Party Rapperswil 2025</p>
<p class="event-entry-location">Rapperswil, Rapperswil-Jona, St. Gallen, Switzerland</p>
</div>
<span class="event-entry-date">
3rd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4049/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Maptime Amsterdam: Autumn mapping party</p>
<p class="event-entry-location">Amsterdam, North Holland, Netherlands</p>
</div>
<span class="event-entry-date">
3rd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3931/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM India Online (Remote) Mapping Party</p>
<p class="event-entry-location">New Delhi, Delhi, India</p>
</div>
<span class="event-entry-date">
5th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3869/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Vortrag - Wie nutze ich OpenStreetMap - BWS Lehen-Vorstadt, Salzburg</p>
<p class="event-entry-location">Salzburg, Salzburg, Austria</p>
</div>
<span class="event-entry-date">
7th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3990/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">East Midlands pub meet-up</p>
<p class="event-entry-location">Derby, England, United Kingdom</p>
</div>
<span class="event-entry-date">
7th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3737/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
</div>
<span class="event-entry-date">
7th October
9th October
@ -673,10 +325,10 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4076/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4074/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">[Online] 🇧🇷 Oficina de validação com editor JOSM</p>
<p class="event-entry-location">Rio de Janeiro, Rio de Janeiro, Brazil</p>
<p class="event-entry-name">Mapathon Bliesgau, Saarpfalz-Kreis</p>
<p class="event-entry-location">Homburg, Saarland, Germany</p>
</div>
<span class="event-entry-date">
@ -705,10 +357,26 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4074/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4079/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mapathon Bliesgau, Saarpfalz-Kreis</p>
<p class="event-entry-location">Homburg, Saarland, Germany</p>
<p class="event-entry-name">OSM-Treffen in Bochum</p>
<p class="event-entry-location">Bochum, North Rhine-Westphalia, Germany</p>
</div>
<span class="event-entry-date">
9th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4076/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">[Online] 🇧🇷 Oficina de validação com editor JOSM</p>
<p class="event-entry-location">Rio de Janeiro, Rio de Janeiro, Brazil</p>
</div>
<span class="event-entry-date">
@ -766,6 +434,38 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4080/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Cartopartie cyclable</p>
<p class="event-entry-location">Étalle, Luxembourg, Belgium</p>
</div>
<span class="event-entry-date">
11th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4099/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM Bhopal Mapping Party 0</p>
<p class="event-entry-location">Bhopal, Madhya Pradesh, India</p>
</div>
<span class="event-entry-date">
11th October
</span>
</a></li>
@ -787,7 +487,7 @@
<li class="event-list-entry"><a href="/event/3997/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">24th OSM Delhi MapWalk (Outer West zone)</p>
<p class="event-entry-name">24th OSM Delhi MapWalk (Dwarka)</p>
<p class="event-entry-location">Delhi, India</p>
</div>
<span class="event-entry-date">
@ -804,7 +504,23 @@
<li class="event-list-entry"><a href="/event/4040/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Missing Maps : Mapathon en ligne - CartONG [fr]</p>
<p class="event-entry-location">Chambéry, Auvergne-Rhône-Alpes, France</p>
</div>
<span class="event-entry-date">
13th October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4088/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">180. OSM-Stammtisch Zürich</p>
<p class="event-entry-location">Zurich, Zurich, Switzerland</p>
</div>
<span class="event-entry-date">
@ -929,10 +645,10 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4051/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4109/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM Mumbai MapWalk #4</p>
<p class="event-entry-location">Mumbai, Maharashtra, India</p>
<p class="event-entry-name">LàO : Libre à Orx</p>
<p class="event-entry-location">Orx, Nouvelle-Aquitaine, France</p>
</div>
<span class="event-entry-date">
@ -945,15 +661,15 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3998/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4051/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">25th OSM Delhi MapWalk (Meerut)</p>
<p class="event-entry-location">Meerut, Uttar Pradesh, India</p>
<p class="event-entry-name">OSM Mumbai MapWalk #4</p>
<p class="event-entry-location">Mumbai, Maharashtra, India</p>
</div>
<span class="event-entry-date">
19th October
18th October
@ -1038,6 +754,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4100/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Intune Core Services GIVE: Missing Maps Mapathon</p>
<p class="event-entry-location">Redmond, Washington, United States</p>
</div>
<span class="event-entry-date">
22nd October
</span>
</a></li>
@ -1086,6 +818,38 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4096/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Missing Maps mapathon Žilina #19</p>
<p class="event-entry-location">Žilina, Region of Žilina, Slovakia</p>
</div>
<span class="event-entry-date">
23rd October
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4107/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Rencontre communauté OSM Tours (Fr)</p>
<p class="event-entry-location">Tours, Centre-Val de Loire, France</p>
</div>
<span class="event-entry-date">
24th October
</span>
</a></li>
@ -1192,6 +956,70 @@
<h3 class="event-list-group-title">November</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/4105/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM-stand @ International Day Les Scouts</p>
<p class="event-entry-location">Courrière, Namur, Belgium</p>
</div>
<span class="event-entry-date">
1st November
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4101/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Braunschweiger Mappertreffen im Stratum0 Hackerspace</p>
<p class="event-entry-location">Brunswick, Lower Saxony, Germany</p>
</div>
<span class="event-entry-date">
1st November
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4110/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
<p class="event-entry-location">New Delhi, India</p>
</div>
<span class="event-entry-date">
2nd November
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4084/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mapathon - Monthly Missing Maps w/ EWB Norway</p>
<p class="event-entry-location">Oslo, Norway</p>
</div>
<span class="event-entry-date">
4th November
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3870/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM Treffen Salzburg</p>
@ -1205,6 +1033,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3738/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
</div>
<span class="event-entry-date">
4th November
</span>
</a></li>
@ -1224,15 +1068,15 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3738/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/4093/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
<p class="event-entry-name">Stuttgarter OpenStreetMap-Treffen</p>
<p class="event-entry-location">Stuttgart, Baden-Württemberg, Germany</p>
</div>
<span class="event-entry-date">
4th November
5th November
@ -1285,6 +1129,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4086/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSMmapperCPH</p>
<p class="event-entry-location">Copenhagen, Capital Region of Denmark, Denmark</p>
</div>
<span class="event-entry-date">
9th November
</span>
</a></li>
@ -1317,6 +1177,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4092/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">181. OSM-Stammtisch Zürich</p>
<p class="event-entry-location">Zurich, Zurich, Switzerland</p>
</div>
<span class="event-entry-date">
11th November
</span>
</a></li>
@ -1541,6 +1417,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4108/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Maptime Amsterdam: Map &amp; Meet</p>
<p class="event-entry-location">Amsterdam, North Holland, Netherlands</p>
</div>
<span class="event-entry-date">
27th November
</span>
</a></li>
@ -1567,6 +1459,22 @@
<h3 class="event-list-group-title">December</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/4085/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mapathon - Monthly Missing Maps w/ EWB Norway</p>
<p class="event-entry-location">Oslo, Norway</p>
</div>
<span class="event-entry-date">
2nd December
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3871/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM Treffen Salzburg</p>
@ -1596,6 +1504,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4104/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">East Midlands pub meet-up</p>
<p class="event-entry-location">Derby, England, United Kingdom</p>
</div>
<span class="event-entry-date">
2nd December
</span>
</a></li>
@ -1612,6 +1536,38 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4102/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Braunschweiger Mappertreffen im Stratum0 Hackerspace</p>
<p class="event-entry-location">Brunswick, Lower Saxony, Germany</p>
</div>
<span class="event-entry-date">
6th December
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4111/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
<p class="event-entry-location">New Delhi, India</p>
</div>
<span class="event-entry-date">
7th December
</span>
</a></li>
@ -1692,6 +1648,22 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4081/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">GeoCamp - State of the Map España 2025</p>
<p class="event-entry-location">Zaragoza, Aragon, Spain</p>
</div>
<span class="event-entry-date">
13th December
</span>
</a></li>
@ -1759,10 +1731,10 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3441/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/3858/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">19. Österreichischer OSM-Stammtisch (online)</p>
<p class="event-entry-location">Stainach-Pürgg, Styria, Austria</p>
<p class="event-entry-name">Düsseldorfer OpenStreetMap-Treffen (online)</p>
<p class="event-entry-location">Dusseldorf, North Rhine-Westphalia, Germany</p>
</div>
<span class="event-entry-date">
@ -1775,10 +1747,10 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3858/" class="event-list-entry-box">
<li class="event-list-entry"><a href="/event/3441/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Düsseldorfer OpenStreetMap-Treffen (online)</p>
<p class="event-entry-location">Dusseldorf, North Rhine-Westphalia, Germany</p>
<p class="event-entry-name">19. Österreichischer OSM-Stammtisch (online)</p>
<p class="event-entry-location">Stainach-Pürgg, Styria, Austria</p>
</div>
<span class="event-entry-date">
@ -1830,6 +1802,22 @@
<h3 class="event-list-group-title">January 2026</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/4112/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
<p class="event-entry-location">New Delhi, India</p>
</div>
<span class="event-entry-date">
4th January
</span>
</a></li>
<li class="event-list-entry"><a href="/event/3790/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OpenStreetMap Midwest Meetup</p>
@ -1843,6 +1831,61 @@
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4103/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">East Midlands pub meet-up</p>
<p class="event-entry-location">Derby, England, United Kingdom</p>
</div>
<span class="event-entry-date">
20th January
</span>
</a></li>
<li class="event-list-entry"><a href="/event/4082/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">Mapping USA 2026</p>
</div>
<span class="event-entry-date">
30th31st January
</span>
</a></li>
</ul>
</li>
<li>
<h3 class="event-list-group-title">February 2026</h3>
<ul class="event-list-group">
<li class="event-list-entry"><a href="/event/4113/" class="event-list-entry-box">
<div class="event-entry-main">
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
<p class="event-entry-location">New Delhi, India</p>
</div>
<span class="event-entry-date">
1st February
</span>
</a></li>

View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
Script de test pour les améliorations du scraper agenda du libre
Démontre les nouvelles fonctionnalités : cache JSON, limitation d'événements, mode dry-run
"""
import subprocess
import sys
import os
def run_test(test_name, command):
"""Exécute un test et affiche les résultats"""
print(f"\n{'='*60}")
print(f"TEST: {test_name}")
print(f"{'='*60}")
print(f"Commande: {' '.join(command)}")
print("-" * 60)
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("STDERR:")
print(result.stderr)
print(f"Code de retour: {result.returncode}")
return result.returncode == 0
except subprocess.TimeoutExpired:
print("TIMEOUT: Le test a pris trop de temps")
return False
except Exception as e:
print(f"ERREUR: {e}")
return False
def main():
"""Exécute une série de tests pour démontrer les améliorations"""
print("🧪 Tests des améliorations du scraper agenda du libre")
print("=" * 60)
# Vérifier que le script existe
script_path = "agendadulibre.py"
if not os.path.exists(script_path):
print(f"❌ Erreur: Le script {script_path} n'existe pas")
sys.exit(1)
tests = [
{
"name": "Test 1: Mode dry-run par défaut (limite 5 événements)",
"command": [sys.executable, script_path, "--max-events", "5", "--verbose"]
},
{
"name": "Test 2: Mode dry-run avec cache (limite 3 événements)",
"command": [sys.executable, script_path, "--max-events", "3", "--verbose"]
},
{
"name": "Test 3: Mode réel (--no-dry-run) avec limite 2 événements",
"command": [sys.executable, script_path, "--no-dry-run", "--max-events", "2", "--verbose"]
},
{
"name": "Test 4: Force refresh avec dry-run",
"command": [sys.executable, script_path, "--force-refresh", "--max-events", "3", "--verbose"]
}
]
results = []
for test in tests:
success = run_test(test["name"], test["command"])
results.append((test["name"], success))
# Résumé des tests
print(f"\n{'='*60}")
print("RÉSUMÉ DES TESTS")
print(f"{'='*60}")
passed = 0
for name, success in results:
status = "✅ PASSÉ" if success else "❌ ÉCHOUÉ"
print(f"{status}: {name}")
if success:
passed += 1
print(f"\nRésultat: {passed}/{len(results)} tests réussis")
if passed == len(results):
print("🎉 Tous les tests sont passés avec succès !")
return 0
else:
print("⚠️ Certains tests ont échoué")
return 1
if __name__ == "__main__":
sys.exit(main())