get agendadulibre
This commit is contained in:
parent
13dc5ceef8
commit
3fa60f3052
9 changed files with 9622 additions and 2278 deletions
2
extractors/.gitignore
vendored
Normal file
2
extractors/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
test_env
|
||||
venv
|
307
extractors/README_agendadulibre_improvements.md
Normal file
307
extractors/README_agendadulibre_improvements.md
Normal 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é.
|
|
@ -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)
|
||||
|
|
|
@ -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
593
extractors/demo_agendadulibre_improvements.py
Normal file
593
extractors/demo_agendadulibre_improvements.py
Normal 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()
|
|
@ -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 August–30th 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">
|
||||
|
||||
|
||||
3rd–5th 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 & 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 & 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">
|
||||
|
||||
|
||||
30th–31st 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>
|
||||
|
||||
|
|
92
extractors/test_agendadulibre_improvements.py
Normal file
92
extractors/test_agendadulibre_improvements.py
Normal 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())
|
Loading…
Add table
Add a link
Reference in a new issue