scrapping agendadulibre

This commit is contained in:
Tykayn 2025-10-04 19:26:00 +02:00 committed by tykayn
parent 6deed13d0b
commit 74738772b4
18 changed files with 63557 additions and 11 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"makefile.configureOnOpen": false
}

114
extractors/Makefile Normal file
View file

@ -0,0 +1,114 @@
# Makefile pour le scraper agenda du libre
.PHONY: help install test demo monitor clean run setup-cron
# Configuration par défaut
API_URL ?= https://api.openeventdatabase.org
BATCH_SIZE ?= 1
help: ## Afficher l'aide
@echo "🔧 Scraper Agenda du Libre - Commandes disponibles"
@echo "=================================================="
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "💡 Variables d'environnement:"
@echo " API_URL URL de l'API OEDB (défaut: https://api.openeventdatabase.org)"
@echo " BATCH_SIZE Taille des batches (défaut: 1)"
@echo ""
@echo "📝 Exemples:"
@echo " make run BATCH_SIZE=5"
@echo " make run API_URL=http://api.example.com:8080"
install: ## Installer les dépendances
@echo "📦 Installation des dépendances..."
sudo apt update
sudo apt install -y python3-requests python3-icalendar
@echo "✅ Dépendances installées"
test: ## Exécuter les tests
@echo "🧪 Exécution des tests..."
python3 test_agendadulibre.py
python3 test_api_connection.py --api-url $(API_URL)
demo: ## Exécuter la démonstration
@echo "🎭 Exécution de la démonstration..."
python3 demo_agendadulibre.py
monitor: ## Afficher les statistiques
@echo "📊 Affichage des statistiques..."
python3 monitor_agendadulibre.py
run: ## Exécuter le scraper
@echo "🚀 Exécution du scraper..."
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE)
run-verbose: ## Exécuter le scraper en mode verbeux
@echo "🚀 Exécution du scraper en mode verbeux..."
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE) --verbose
run-force: ## Exécuter le scraper en forçant le rechargement
@echo "🚀 Exécution du scraper avec rechargement forcé..."
python3 agendadulibre.py --api-url $(API_URL) --batch-size $(BATCH_SIZE) --force-refresh
run-cache-test: ## Tester le système de cache
@echo "🔄 Test du système de cache..."
python3 agendadulibre.py --api-url $(API_URL) --batch-size 1 --verbose
@echo " Premier appel terminé, deuxième appel (depuis cache)..."
python3 agendadulibre.py --api-url $(API_URL) --batch-size 1 --verbose
setup-cron: ## Configurer la planification cron
@echo "⏰ Configuration de la planification cron..."
./setup_cron.sh
clean: ## Nettoyer les fichiers temporaires
@echo "🧹 Nettoyage des fichiers temporaires..."
rm -f agendadulibre_events.json
rm -f agendadulibre_events.ics
rm -f agendadulibre_scraper.log
rm -f cron_agendadulibre.log
@echo "✅ Nettoyage terminé"
status: ## Afficher le statut du système
@echo "📊 Statut du système:"
@echo "===================="
@echo "📁 Fichier de données:"
@if [ -f agendadulibre_events.json ]; then \
SIZE=$$(stat -c%s agendadulibre_events.json 2>/dev/null || echo "0"); \
echo " ✅ Présent ($$SIZE octets)"; \
else \
echo " ❌ Absent"; \
fi
@echo "📁 Fichier cache iCal:"
@if [ -f agendadulibre_events.ics ]; then \
SIZE=$$(stat -c%s agendadulibre_events.ics 2>/dev/null || echo "0"); \
DATE=$$(stat -c%y agendadulibre_events.ics 2>/dev/null || echo "inconnue"); \
echo " ✅ Présent ($$SIZE octets)"; \
echo " 📅 Modifié: $$DATE"; \
else \
echo " ❌ Absent"; \
fi
@echo ""
@echo "📋 Tâches cron:"
@if crontab -l 2>/dev/null | grep -q agendadulibre; then \
echo " ✅ Tâches configurées:"; \
crontab -l | grep agendadulibre | sed 's/^/ /'; \
else \
echo " ❌ Aucune tâche configurée"; \
fi
@echo ""
@echo "🔗 Connexion API:"
@python3 -c "import requests; print(' ✅' if requests.get('$(API_URL)', timeout=5).status_code == 200 else ' ❌')" 2>/dev/null || echo " ❌ API non accessible"
logs: ## Afficher les logs récents
@echo "📋 Logs récents:"
@echo "==============="
@if [ -f agendadulibre_scraper.log ]; then \
tail -20 agendadulibre_scraper.log; \
else \
echo "Aucun fichier de log trouvé"; \
fi
check-deps: ## Vérifier les dépendances
@echo "🔍 Vérification des dépendances..."
@python3 -c "import requests, icalendar; print('✅ Toutes les dépendances sont disponibles')" || echo "❌ Dépendances manquantes - exécutez 'make install'"

View file

@ -0,0 +1,161 @@
# Scraper Agenda du Libre
Script de scraping pour récupérer les événements de l'agenda du libre (https://www.agendadulibre.org/) et les envoyer à l'API OEDB.
## Fonctionnalités
- 📥 Récupération automatique du fichier iCal depuis l'agenda du libre
- 🔄 Traitement par batch configurable
- 💾 Sauvegarde locale de l'état des événements (JSON)
- 🚫 Évite les doublons (ne renvoie pas les événements déjà traités)
- 📊 Statistiques détaillées et logging
- 🧪 Mode démo et tests inclus
## Installation
1. Installer les dépendances Python :
```bash
pip install -r requirements_agendadulibre.txt
```
2. Rendre les scripts exécutables :
```bash
chmod +x agendadulibre.py
chmod +x test_agendadulibre.py
chmod +x demo_agendadulibre.py
```
## Utilisation
### Scraping complet
```bash
# Utilisation basique (1 événement par batch)
python agendadulibre.py
# Avec options personnalisées
python agendadulibre.py --api-url https://api.openeventdatabase.org --batch-size 5 --verbose
```
### Options disponibles
- `--api-url` : URL de base de l'API OEDB (défaut: https://api.openeventdatabase.org)
- `--batch-size` : Nombre d'événements à traiter par batch (défaut: 1)
- `--verbose` : Mode verbeux pour plus de détails
- `--force-refresh` : Forcer le rechargement du fichier iCal (ignorer le cache)
- `--cache-duration` : Durée de validité du cache en heures (défaut: 1)
### Démonstration
```bash
# Mode démo (ne fait pas d'appels API réels)
python demo_agendadulibre.py
```
### Tests
```bash
# Exécuter les tests
python test_agendadulibre.py
```
## Fichiers générés
- `agendadulibre_events.json` : Base de données locale des événements traités
- `agendadulibre_events.ics` : Cache local du fichier iCal (valide 1h)
- `agendadulibre_scraper.log` : Logs détaillés du scraper
## Système de cache iCal
Le script utilise un système de cache intelligent pour éviter de télécharger le fichier iCal à chaque exécution :
- **Cache valide** : Le fichier iCal est mis en cache localement pendant 1 heure par défaut
- **Rechargement automatique** : Si le cache est expiré, le fichier est automatiquement rechargé
- **Fallback** : En cas d'erreur de téléchargement, le script utilise le cache même s'il est expiré
- **Force refresh** : Option `--force-refresh` pour ignorer le cache et forcer le rechargement
### Avantages du cache
- ⚡ **Performance** : Évite les téléchargements inutiles
- 🔄 **Fiabilité** : Fonctionne même si l'API iCal est temporairement indisponible
- 📊 **Efficacité** : Réduit la charge sur le serveur de l'agenda du libre
## Format des événements
Les événements sont convertis au format OEDB avec les propriétés suivantes :
```json
{
"properties": {
"label": "Titre de l'événement",
"description": "Description de l'événement",
"what": "culture.geek",
"where": "Lieu de l'événement",
"start": "2024-01-01T10:00:00",
"stop": "2024-01-01T12:00:00",
"url": "https://www.agendadulibre.org/event/123",
"source": "agendadulibre.org",
"last_modified_by": "agendadulibre_scraper"
},
"geometry": {
"type": "Point",
"coordinates": [0, 0]
}
}
```
## Gestion des doublons
Le script utilise un système de suivi local pour éviter les doublons :
- Chaque événement reçoit un ID unique basé sur son contenu
- Les événements déjà traités avec succès ne sont pas renvoyés
- Les événements en erreur peuvent être retentés
- Les événements déjà existants (réponse 409) sont marqués comme traités
## Statuts des événements
- `saved` : Événement envoyé avec succès à l'API
- `already_exists` : Événement déjà existant dans l'API (réponse 409)
- `error` : Erreur lors de l'envoi à l'API
## Exemple de sortie
```
2024-01-01 10:00:00 - INFO - 🚀 Démarrage du scraping de l'agenda du libre
2024-01-01 10:00:01 - INFO - Récupération du fichier iCal depuis https://www.agendadulibre.org/events.ics
2024-01-01 10:00:02 - INFO - Fichier iCal récupéré avec succès
2024-01-01 10:00:03 - INFO - Traitement de 15 nouveaux événements par batch de 1
2024-01-01 10:00:04 - INFO - Envoi de l'événement: Conférence Python
2024-01-01 10:00:05 - INFO - ✅ Conférence Python - Créé avec succès
...
2024-01-01 10:00:30 - INFO - 📊 Statistiques finales:
2024-01-01 10:00:30 - INFO - Total d'événements trouvés: 25
2024-01-01 10:00:30 - INFO - Nouveaux événements envoyés: 12
2024-01-01 10:00:30 - INFO - Événements déjà existants: 8
2024-01-01 10:00:30 - INFO - Erreurs d'API: 2
2024-01-01 10:00:30 - INFO - Erreurs de parsing: 3
2024-01-01 10:00:30 - INFO - Événements envoyés cette fois: 12
2024-01-01 10:00:30 - INFO - ✅ Scraping terminé avec succès
```
## Planification
Pour automatiser le scraping, vous pouvez utiliser cron :
```bash
# Exécuter toutes les heures
0 * * * * cd /path/to/extractors && python agendadulibre.py --batch-size 5
# Exécuter tous les jours à 6h
0 6 * * * cd /path/to/extractors && python agendadulibre.py --batch-size 10
```
## Dépannage
### Erreur de connexion à l'API
- Vérifiez que l'API OEDB est démarrée
- Vérifiez l'URL de l'API avec `--api-url`
### Erreur de parsing iCal
- Vérifiez la connectivité internet
- Vérifiez que l'URL iCal est accessible
### Événements non géocodés
- Les événements sont créés avec des coordonnées par défaut [0, 0]
- Un processus de géocodage séparé peut être ajouté si nécessaire

402
extractors/agendadulibre.py Normal file
View file

@ -0,0 +1,402 @@
#!/usr/bin/env python3
"""
Script de scraping pour l'agenda du libre (https://www.agendadulibre.org/)
Utilise le fichier iCal pour récupérer les événements et les envoyer à l'API OEDB
"""
import requests
import json
import os
import sys
import argparse
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import icalendar
from icalendar import Calendar, Event
import logging
# Configuration par défaut
api_oedb = "https://api.openeventdatabase.org"
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('agendadulibre_scraper.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
class AgendaDuLibreScraper:
def __init__(self, api_base_url: str = api_oedb, batch_size: int = 1):
self.api_base_url = api_base_url
self.batch_size = batch_size
self.data_file = "agendadulibre_events.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()
def load_events_data(self) -> Dict:
"""Charge les données d'événements depuis le fichier JSON local"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error(f"Erreur lors du chargement du fichier {self.data_file}: {e}")
return {"events": {}, "last_update": None}
return {"events": {}, "last_update": None}
def save_events_data(self):
"""Sauvegarde les données d'événements dans le fichier JSON local"""
try:
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.events_data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du fichier {self.data_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):
return False
try:
file_time = os.path.getmtime(self.ical_file)
cache_age = datetime.now().timestamp() - file_time
cache_age_hours = cache_age / 3600
logger.debug(f"Cache iCal âgé de {cache_age_hours:.2f} heures")
return cache_age_hours < self.cache_duration_hours
except Exception as e:
logger.error(f"Erreur lors de la vérification du cache iCal: {e}")
return False
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}")
except Exception as e:
logger.error(f"Erreur lors de la sauvegarde du cache iCal: {e}")
def load_ical_cache(self) -> Optional[bytes]:
"""Charge le contenu iCal depuis le cache local"""
try:
with open(self.ical_file, 'rb') as f:
content = f.read()
logger.info(f"Cache iCal chargé depuis {self.ical_file}")
return content
except Exception as e:
logger.error(f"Erreur lors du chargement du cache iCal: {e}")
return None
def fetch_ical_data(self, force_refresh: bool = False) -> Optional[Calendar]:
"""Récupère et parse le fichier iCal depuis l'agenda du libre ou depuis le cache"""
ical_content = None
# Vérifier si le cache est valide (sauf si on force le rechargement)
if not force_refresh and self.is_ical_cache_valid():
logger.info("Utilisation du cache iCal local (moins d'une heure)")
ical_content = self.load_ical_cache()
else:
if force_refresh:
logger.info(f"Rechargement forcé du fichier iCal depuis {self.ical_url}")
else:
logger.info(f"Cache iCal expiré ou absent, téléchargement depuis {self.ical_url}")
try:
response = requests.get(self.ical_url, timeout=30)
response.raise_for_status()
ical_content = response.content
# 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}")
# Essayer de charger depuis le cache même s'il est expiré
logger.info("Tentative de chargement depuis le cache expiré...")
ical_content = self.load_ical_cache()
if ical_content is None:
logger.error("Impossible de récupérer le contenu iCal")
return None
try:
calendar = Calendar.from_ical(ical_content)
logger.info(f"Fichier iCal parsé avec succès")
return calendar
except Exception as e:
logger.error(f"Erreur lors du parsing du fichier iCal: {e}")
return None
def parse_event(self, event: Event) -> Optional[Dict]:
"""Parse un événement iCal et le convertit au format OEDB"""
try:
# Récupérer les propriétés de base
summary = str(event.get('summary', ''))
description = str(event.get('description', ''))
location = str(event.get('location', ''))
url = str(event.get('url', ''))
# Gestion des dates
dtstart = event.get('dtstart')
dtend = event.get('dtend')
if not dtstart:
logger.warning(f"Événement sans date de début: {summary}")
return None
# Convertir les dates
start_date = dtstart.dt
if isinstance(start_date, datetime):
start_iso = start_date.isoformat()
else:
# Date seulement (sans heure)
start_iso = f"{start_date}T00:00:00"
end_date = None
if dtend:
end_dt = dtend.dt
if isinstance(end_dt, datetime):
end_iso = end_dt.isoformat()
else:
end_iso = f"{end_dt}T23:59:59"
else:
# Si pas de date de fin, ajouter 2 heures par défaut
if isinstance(start_date, datetime):
end_iso = (start_date + timedelta(hours=2)).isoformat()
else:
end_iso = f"{start_date}T02:00:00"
# Créer l'événement au format OEDB
oedb_event = {
"properties": {
"label": summary,
"description": description,
"type": "scheduled",
"what": "culture.floss", # Type par défaut pour l'agenda du libre
"where": location,
"start": start_iso,
"stop": end_iso,
"url": url if url else None,
"source:name": "Agenda du Libre",
"source:url": "https://www.agendadulibre.org/",
"last_modified_by": "agendadulibre_scraper"
},
"geometry": {
"type": "Point",
"coordinates": [0, 0] # Coordonnées par défaut, à géocoder si nécessaire
}
}
# Créer un ID unique basé sur le contenu
event_id = self.generate_event_id(summary, start_iso, location)
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 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"""
try:
url = f"{self.api_base_url}/event"
headers = {"Content-Type": "application/json"}
# Formater l'événement au format GeoJSON attendu par l'API
geojson_event = {
"type": "Feature",
"geometry": event_data["event"]["geometry"],
"properties": event_data["event"]["properties"]
}
response = requests.post(url, json=geojson_event, headers=headers, timeout=30)
if response.status_code == 201:
return True, "Créé avec succès"
elif response.status_code == 409:
return False, "Événement déjà existant"
else:
return False, f"Erreur API: {response.status_code} - {response.text}"
except requests.RequestException as e:
return False, f"Erreur de connexion: {e}"
except Exception as e:
return False, f"Erreur inattendue: {e}"
def process_events(self, calendar: Calendar) -> Dict:
"""Traite tous les événements du calendrier"""
stats = {
"total_events": 0,
"new_events": 0,
"already_saved": 0,
"api_errors": 0,
"parse_errors": 0,
"sent_this_run": 0
}
events_to_process = []
# Parcourir tous les événements
for component in calendar.walk():
if component.name == "VEVENT":
stats["total_events"] += 1
# Parser l'événement
parsed_event = self.parse_event(component)
if not parsed_event:
stats["parse_errors"] += 1
continue
event_id = parsed_event["id"]
# Vérifier si l'événement existe déjà dans nos données
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']}")
continue
events_to_process.append(parsed_event)
# Traiter les événements par batch
logger.info(f"Traitement de {len(events_to_process)} nouveaux événements par batch de {self.batch_size}")
for i in range(0, len(events_to_process), self.batch_size):
batch = events_to_process[i:i + self.batch_size]
logger.info(f"Traitement du batch {i//self.batch_size + 1}/{(len(events_to_process) + self.batch_size - 1)//self.batch_size}")
for event_data in batch:
event_id = event_data["id"]
event_label = event_data["event"]["properties"]["label"]
logger.info(f"Envoi de l'événement: {event_label}")
# Envoyer à l'API
success, message = self.send_event_to_api(event_data)
# Mettre à jour les statistiques et les données locales
if success:
stats["new_events"] += 1
stats["sent_this_run"] += 1
self.events_data["events"][event_id] = {
"status": "saved",
"message": message,
"last_attempt": datetime.now().isoformat(),
"event": event_data["event"]
}
logger.info(f"{event_label} - {message}")
else:
if "déjà existant" in message or "already exists" in message.lower():
stats["already_saved"] += 1
self.events_data["events"][event_id] = {
"status": "already_exists",
"message": message,
"last_attempt": datetime.now().isoformat(),
"event": event_data["event"]
}
logger.info(f"⚠️ {event_label} - {message}")
else:
stats["api_errors"] += 1
self.events_data["events"][event_id] = {
"status": "error",
"message": message,
"last_attempt": datetime.now().isoformat(),
"event": event_data["event"]
}
logger.error(f"{event_label} - {message}")
# Mettre à jour la date de dernière mise à jour
self.events_data["last_update"] = datetime.now().isoformat()
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"Cache iCal: {'ignoré' if force_refresh else f'valide pendant {self.cache_duration_hours}h'}")
# Récupérer le fichier iCal
calendar = self.fetch_ical_data(force_refresh=force_refresh)
if not calendar:
logger.error("❌ Impossible de récupérer le fichier iCal")
return False
# Traiter les événements
stats = self.process_events(calendar)
# Sauvegarder les données
self.save_events_data()
# Afficher les statistiques finales
logger.info("📊 Statistiques finales:")
logger.info(f" Total d'événements trouvés: {stats['total_events']}")
logger.info(f" Nouveaux événements envoyés: {stats['new_events']}")
logger.info(f" Événements déjà existants: {stats['already_saved']}")
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']}")
logger.info("✅ Scraping terminé avec succès")
return True
def main():
parser = argparse.ArgumentParser(description="Scraper pour l'agenda du libre")
parser.add_argument("--api-url", default=api_oedb,
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("--verbose", "-v", action="store_true",
help="Mode verbeux")
parser.add_argument("--force-refresh", "-f", action="store_true",
help="Forcer le rechargement du fichier iCal (ignorer le cache)")
parser.add_argument("--cache-duration", type=int, default=1,
help="Durée de validité du cache en heures (défaut: 1)")
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Créer et exécuter le scraper
scraper = AgendaDuLibreScraper(
api_base_url=args.api_url,
batch_size=args.batch_size
)
# Modifier la durée de cache si spécifiée
scraper.cache_duration_hours = args.cache_duration
# Exécuter avec ou sans rechargement forcé
success = scraper.run(force_refresh=args.force_refresh)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Script de démonstration pour le scraper de l'agenda du libre
Mode dry-run pour tester sans envoyer de données à l'API
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from agendadulibre import AgendaDuLibreScraper, api_oedb
import logging
class DemoAgendaDuLibreScraper(AgendaDuLibreScraper):
"""Version démo du scraper qui n'envoie pas de données à l'API"""
def send_event_to_api(self, event_data):
"""Version démo qui simule l'envoi à l'API"""
event_label = event_data["event"]["properties"]["label"]
print(f"🔍 [DEMO] Simulation d'envoi: {event_label}")
# Simuler différents types de réponses
import random
responses = [
(True, "Créé avec succès"),
(False, "Événement déjà existant"),
(True, "Créé avec succès"),
(False, "Erreur API: 500 - Internal Server Error")
]
success, message = random.choice(responses)
if success:
print(f"✅ [DEMO] {event_label} - {message}")
else:
print(f"❌ [DEMO] {event_label} - {message}")
return success, message
def main():
"""Exécute la démonstration"""
print("🎭 Démonstration du scraper agenda du libre (mode dry-run)")
print("=" * 60)
# Configuration du logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Créer le scraper en mode démo
scraper = DemoAgendaDuLibreScraper(
api_base_url=api_oedb, # Utiliser l'URL par défaut
batch_size=2 # Traiter 2 événements par batch pour la démo
)
print(f"📋 Configuration:")
print(f" - URL iCal: {scraper.ical_url}")
print(f" - Taille des batches: {scraper.batch_size}")
print(f" - Fichier de données: {scraper.data_file}")
print(f" - Fichier cache iCal: {scraper.ical_file}")
print(f" - Durée de cache: {scraper.cache_duration_hours}h")
print()
# Récupérer le fichier iCal
print("📥 Récupération du fichier iCal...")
calendar = scraper.fetch_ical_data()
if not calendar:
print("❌ Impossible de récupérer le fichier iCal")
return False
# Compter les événements
event_count = 0
for component in calendar.walk():
if component.name == "VEVENT":
event_count += 1
print(f"📅 {event_count} événements trouvés dans le fichier iCal")
# Test du cache
print("\n🔄 Test du cache iCal...")
print(" Premier appel (téléchargement)...")
calendar2 = scraper.fetch_ical_data()
print(" Deuxième appel (depuis le cache)...")
calendar3 = scraper.fetch_ical_data()
print(" ✅ Cache fonctionne correctement")
print()
# Traiter seulement les 5 premiers événements pour la démo
print("🔄 Traitement des 5 premiers événements (démo)...")
print("-" * 40)
processed = 0
for component in calendar.walk():
if component.name == "VEVENT" and processed < 5:
parsed_event = scraper.parse_event(component)
if parsed_event:
event_label = parsed_event["event"]["properties"]["label"]
start_date = parsed_event["event"]["properties"]["start"]
location = parsed_event["event"]["properties"]["where"]
print(f"📝 Événement {processed + 1}:")
print(f" Titre: {event_label}")
print(f" Date: {start_date}")
print(f" Lieu: {location}")
print()
# Simuler l'envoi
success, message = scraper.send_event_to_api(parsed_event)
# Mettre à jour les données locales (simulation)
event_id = parsed_event["id"]
scraper.events_data["events"][event_id] = {
"status": "saved" if success else "error",
"message": message,
"last_attempt": "2024-01-01T00:00:00",
"event": parsed_event["event"]
}
processed += 1
print("-" * 40)
print(f"✅ Démonstration terminée - {processed} événements traités")
print()
print("💡 Pour exécuter le vrai scraper:")
print(" python agendadulibre.py --batch-size 5 --api-url http://localhost:5000")
print()
print("🧪 Pour exécuter les tests:")
print(" python test_agendadulibre.py")
return True
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View file

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Script pour extraire les coordonnées géographiques du fichier ICS de l'agenda du libre.
Le fichier ICS contient des coordonnées dans les propriétés X-ALT-DESC;FMTTYPE=text/html
sous forme d'attributs data-latitude et data-longitude.
"""
import re
import json
from typing import List, Dict, Tuple
def extract_coordinates_from_ics(ics_file_path: str) -> List[Dict]:
"""
Extrait les coordonnées géographiques du fichier ICS.
Args:
ics_file_path: Chemin vers le fichier ICS
Returns:
Liste de dictionnaires contenant les coordonnées et informations associées
"""
coordinates = []
with open(ics_file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Recherche des patterns data-latitude et data-longitude
# Pattern pour capturer les coordonnées dans le HTML
pattern = r'data-latitude="([^"]+)"[^>]*data-longitude="([^"]+)"'
matches = re.findall(pattern, content)
for i, (lat, lon) in enumerate(matches):
try:
lat_float = float(lat)
lon_float = float(lon)
# Vérifier que les coordonnées sont valides
if -90 <= lat_float <= 90 and -180 <= lon_float <= 180:
coordinates.append({
'id': i + 1,
'latitude': lat_float,
'longitude': lon_float,
'source': 'agendadulibre_ics'
})
except ValueError:
# Ignorer les coordonnées invalides
continue
return coordinates
def find_events_with_coordinates(ics_file_path: str) -> List[Dict]:
"""
Trouve les événements qui ont des coordonnées dans le fichier ICS.
Args:
ics_file_path: Chemin vers le fichier ICS
Returns:
Liste des événements avec leurs coordonnées
"""
events = []
with open(ics_file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Diviser le contenu en événements individuels
event_blocks = content.split('BEGIN:VEVENT')
for i, block in enumerate(event_blocks[1:], 1): # Ignorer le premier bloc (en-tête)
# Rechercher les coordonnées dans ce bloc d'événement
lat_match = re.search(r'data-latitude="([^"]+)"', block)
lon_match = re.search(r'data-longitude="([^"]+)"', block)
if lat_match and lon_match:
try:
lat = float(lat_match.group(1))
lon = float(lon_match.group(1))
# Vérifier que les coordonnées sont valides
if -90 <= lat <= 90 and -180 <= lon <= 180:
# Extraire d'autres informations de l'événement
summary_match = re.search(r'SUMMARY:(.+)', block)
location_match = re.search(r'LOCATION:(.+)', block)
description_match = re.search(r'DESCRIPTION:(.+)', block)
event = {
'event_id': i,
'latitude': lat,
'longitude': lon,
'summary': summary_match.group(1).strip() if summary_match else '',
'location': location_match.group(1).strip() if location_match else '',
'description': description_match.group(1).strip() if description_match else '',
'source': 'agendadulibre_ics'
}
events.append(event)
except ValueError:
continue
return events
def main():
"""Fonction principale pour extraire et afficher les coordonnées."""
ics_file = 'agendadulibre_events.ics'
print("🔍 Extraction des coordonnées du fichier ICS...")
# Extraire toutes les coordonnées
all_coordinates = extract_coordinates_from_ics(ics_file)
print(f"📍 {len(all_coordinates)} coordonnées trouvées")
# Extraire les événements avec coordonnées
events_with_coords = find_events_with_coordinates(ics_file)
print(f"🎯 {len(events_with_coords)} événements avec coordonnées trouvés")
# Afficher quelques exemples
print("\n📋 Exemples d'événements avec coordonnées :")
for event in events_with_coords[:5]:
print(f"{event['summary']}")
print(f" 📍 {event['latitude']}, {event['longitude']}")
print(f" 📍 Lieu: {event['location']}")
print()
# Sauvegarder les résultats
with open('extracted_coordinates.json', 'w', encoding='utf-8') as f:
json.dump(events_with_coords, f, indent=2, ensure_ascii=False)
print(f"💾 Résultats sauvegardés dans 'extracted_coordinates.json'")
# Statistiques
unique_coords = set((event['latitude'], event['longitude']) for event in events_with_coords)
print(f"📊 {len(unique_coords)} coordonnées uniques")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""
Script de monitoring pour le scraper agenda du libre
Affiche les statistiques et l'état du scraper
"""
import json
import os
import sys
from datetime import datetime, timedelta
from typing import Dict, Any
class AgendaDuLibreMonitor:
def __init__(self, data_file: str = "agendadulibre_events.json"):
self.data_file = data_file
self.events_data = self.load_events_data()
def load_events_data(self) -> Dict[str, Any]:
"""Charge les données d'événements"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"❌ Erreur lors du chargement de {self.data_file}: {e}")
return {"events": {}, "last_update": None}
return {"events": {}, "last_update": None}
def get_statistics(self) -> Dict[str, Any]:
"""Calcule les statistiques des événements"""
events = self.events_data.get("events", {})
stats = {
"total_events": len(events),
"saved": 0,
"already_exists": 0,
"error": 0,
"unknown": 0,
"recent_errors": 0,
"last_update": self.events_data.get("last_update"),
"events_by_status": {},
"recent_events": []
}
# Analyser les statuts
for event_id, event_data in events.items():
status = event_data.get("status", "unknown")
stats["events_by_status"][status] = stats["events_by_status"].get(status, 0) + 1
if status == "saved":
stats["saved"] += 1
elif status == "already_exists":
stats["already_exists"] += 1
elif status == "error":
stats["error"] += 1
else:
stats["unknown"] += 1
# Vérifier les erreurs récentes (dernières 24h)
last_attempt = event_data.get("last_attempt")
if last_attempt and status == "error":
try:
attempt_time = datetime.fromisoformat(last_attempt.replace('Z', '+00:00'))
if datetime.now() - attempt_time.replace(tzinfo=None) < timedelta(hours=24):
stats["recent_errors"] += 1
except:
pass
# Collecter les événements récents (derniers 10)
if len(stats["recent_events"]) < 10:
event_info = {
"id": event_id,
"label": event_data.get("event", {}).get("properties", {}).get("label", "Sans titre"),
"status": status,
"last_attempt": last_attempt,
"message": event_data.get("message", "")
}
stats["recent_events"].append(event_info)
return stats
def display_statistics(self):
"""Affiche les statistiques de manière formatée"""
stats = self.get_statistics()
print("📊 Statistiques du scraper agenda du libre")
print("=" * 50)
# Informations générales
print(f"📁 Fichier de données: {self.data_file}")
print(f"📅 Dernière mise à jour: {stats['last_update'] or 'Jamais'}")
print(f"📈 Total d'événements traités: {stats['total_events']}")
print()
# Répartition par statut
print("📋 Répartition par statut:")
for status, count in stats["events_by_status"].items():
emoji = {
"saved": "",
"already_exists": "⚠️",
"error": "",
"unknown": ""
}.get(status, "")
print(f" {emoji} {status}: {count}")
print()
# Erreurs récentes
if stats["recent_errors"] > 0:
print(f"🚨 Erreurs récentes (24h): {stats['recent_errors']}")
print()
# Événements récents
if stats["recent_events"]:
print("🕒 Événements récents:")
for event in stats["recent_events"][:5]:
emoji = {
"saved": "",
"already_exists": "⚠️",
"error": "",
"unknown": ""
}.get(event["status"], "")
print(f" {emoji} {event['label'][:50]}{'...' if len(event['label']) > 50 else ''}")
if event["status"] == "error":
print(f" 💬 {event['message']}")
print()
# Recommandations
self.display_recommendations(stats)
def display_recommendations(self, stats: Dict[str, Any]):
"""Affiche des recommandations basées sur les statistiques"""
print("💡 Recommandations:")
if stats["total_events"] == 0:
print(" - Aucun événement traité. Exécutez le scraper pour commencer.")
elif stats["error"] > stats["saved"]:
print(" - Beaucoup d'erreurs détectées. Vérifiez la connectivité API.")
elif stats["recent_errors"] > 5:
print(" - Erreurs récentes nombreuses. Vérifiez les logs.")
elif stats["saved"] > 0:
print(" - Scraper fonctionne correctement.")
if stats["already_exists"] > stats["saved"]:
print(" - Beaucoup d'événements déjà existants. Le système de déduplication fonctionne.")
print()
def check_file_status(self):
"""Vérifie l'état du fichier de données"""
if not os.path.exists(self.data_file):
print(f"❌ Fichier de données non trouvé: {self.data_file}")
return False
try:
stat = os.stat(self.data_file)
size = stat.st_size
mtime = datetime.fromtimestamp(stat.st_mtime)
print(f"📁 État du fichier de données:")
print(f" - Taille: {size:,} octets")
print(f" - Dernière modification: {mtime}")
print(f" - Lisible: {'' if os.access(self.data_file, os.R_OK) else ''}")
print(f" - Écriture: {'' if os.access(self.data_file, os.W_OK) else ''}")
print()
return True
except Exception as e:
print(f"❌ Erreur lors de la vérification du fichier: {e}")
return False
def show_help(self):
"""Affiche l'aide"""
print("🔍 Monitor agenda du libre - Aide")
print("=" * 40)
print("Usage: python3 monitor_agendadulibre.py [options]")
print()
print("Options:")
print(" --stats, -s Afficher les statistiques (défaut)")
print(" --file, -f Vérifier l'état du fichier")
print(" --help, -h Afficher cette aide")
print()
print("Exemples:")
print(" python3 monitor_agendadulibre.py")
print(" python3 monitor_agendadulibre.py --file")
print(" python3 monitor_agendadulibre.py --stats")
def main():
"""Fonction principale"""
import argparse
parser = argparse.ArgumentParser(description="Monitor pour le scraper agenda du libre")
parser.add_argument("--stats", "-s", action="store_true", default=True,
help="Afficher les statistiques")
parser.add_argument("--file", "-f", action="store_true",
help="Vérifier l'état du fichier de données")
parser.add_argument("--data-file", default="agendadulibre_events.json",
help="Fichier de données à analyser")
args = parser.parse_args()
monitor = AgendaDuLibreMonitor(args.data_file)
if args.file:
monitor.check_file_status()
if args.stats:
monitor.display_statistics()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,2 @@
requests>=2.28.0
icalendar>=5.0.0

98
extractors/setup_cron.sh Executable file
View file

@ -0,0 +1,98 @@
#!/bin/bash
# Script de configuration du cron pour le scraper agenda du libre
SCRIPT_DIR="/home/poule/encrypted/stockage-syncable/www/development/html/oedb-backend/extractors"
SCRIPT_PATH="$SCRIPT_DIR/agendadulibre.py"
LOG_FILE="$SCRIPT_DIR/cron_agendadulibre.log"
echo "🔧 Configuration du cron pour le scraper agenda du libre"
echo "========================================================"
# Vérifier que le script existe
if [ ! -f "$SCRIPT_PATH" ]; then
echo "❌ Erreur: Le script $SCRIPT_PATH n'existe pas"
exit 1
fi
# Rendre le script exécutable
chmod +x "$SCRIPT_PATH"
echo "📋 Options de planification disponibles:"
echo "1. Toutes les heures (batch de 1)"
echo "2. Toutes les 2 heures (batch de 5)"
echo "3. Tous les jours à 6h (batch de 10)"
echo "4. Tous les jours à 6h et 18h (batch de 5)"
echo "5. Personnalisé"
echo ""
read -p "Choisissez une option (1-5): " choice
case $choice in
1)
CRON_SCHEDULE="0 * * * *"
BATCH_SIZE="1"
;;
2)
CRON_SCHEDULE="0 */2 * * *"
BATCH_SIZE="5"
;;
3)
CRON_SCHEDULE="0 6 * * *"
BATCH_SIZE="10"
;;
4)
CRON_SCHEDULE="0 6,18 * * *"
BATCH_SIZE="5"
;;
5)
read -p "Entrez la planification cron (ex: 0 */3 * * *): " CRON_SCHEDULE
read -p "Entrez la taille des batches (ex: 5): " BATCH_SIZE
;;
*)
echo "❌ Option invalide"
exit 1
;;
esac
# Demander l'URL de l'API
read -p "Entrez l'URL de l'API OEDB (défaut: http://localhost:5000): " API_URL
API_URL=${API_URL:-"http://localhost:5000"}
# Créer la commande cron
CRON_COMMAND="$CRON_SCHEDULE cd $SCRIPT_DIR && python3 $SCRIPT_PATH --api-url $API_URL --batch-size $BATCH_SIZE >> $LOG_FILE 2>&1"
echo ""
echo "📝 Configuration cron proposée:"
echo "Planification: $CRON_SCHEDULE"
echo "Commande: $CRON_COMMAND"
echo ""
read -p "Voulez-vous ajouter cette tâche au cron ? (y/N): " confirm
if [[ $confirm =~ ^[Yy]$ ]]; then
# Ajouter la tâche au cron
(crontab -l 2>/dev/null; echo "$CRON_COMMAND") | crontab -
if [ $? -eq 0 ]; then
echo "✅ Tâche cron ajoutée avec succès"
echo ""
echo "📋 Tâches cron actuelles:"
crontab -l | grep agendadulibre || echo "Aucune tâche trouvée"
echo ""
echo "📁 Logs disponibles dans: $LOG_FILE"
echo "🔍 Pour surveiller les logs: tail -f $LOG_FILE"
else
echo "❌ Erreur lors de l'ajout de la tâche cron"
exit 1
fi
else
echo "❌ Configuration annulée"
exit 0
fi
echo ""
echo "💡 Commandes utiles:"
echo " - Voir les tâches cron: crontab -l"
echo " - Supprimer une tâche: crontab -e"
echo " - Voir les logs: tail -f $LOG_FILE"
echo " - Tester manuellement: python3 $SCRIPT_PATH --api-url $API_URL --batch-size $BATCH_SIZE"

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
Script de test pour le scraper de l'agenda du libre
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from agendadulibre import AgendaDuLibreScraper, api_oedb
import logging
# Configuration du logging pour les tests
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def test_ical_fetch():
"""Test de récupération du fichier iCal"""
print("🧪 Test de récupération du fichier iCal...")
scraper = AgendaDuLibreScraper()
calendar = scraper.fetch_ical_data()
if calendar:
print("✅ Fichier iCal récupéré avec succès")
# Compter les événements
event_count = 0
for component in calendar.walk():
if component.name == "VEVENT":
event_count += 1
print(f"📅 Nombre d'événements trouvés: {event_count}")
return True
else:
print("❌ Échec de la récupération du fichier iCal")
return False
def test_event_parsing():
"""Test de parsing d'un événement"""
print("🧪 Test de parsing d'événement...")
scraper = AgendaDuLibreScraper()
calendar = scraper.fetch_ical_data()
if not calendar:
print("❌ Impossible de récupérer le fichier iCal pour le test")
return False
# Trouver le premier événement
for component in calendar.walk():
if component.name == "VEVENT":
parsed_event = scraper.parse_event(component)
if parsed_event:
print("✅ Événement parsé avec succès:")
print(f" ID: {parsed_event['id']}")
print(f" Titre: {parsed_event['event']['properties']['label']}")
print(f" Début: {parsed_event['event']['properties']['start']}")
print(f" Fin: {parsed_event['event']['properties']['stop']}")
print(f" Lieu: {parsed_event['event']['properties']['where']}")
return True
else:
print("❌ Échec du parsing de l'événement")
return False
print("❌ Aucun événement trouvé pour le test")
return False
def test_data_persistence():
"""Test de persistance des données"""
print("🧪 Test de persistance des données...")
scraper = AgendaDuLibreScraper()
# Test de sauvegarde
test_data = {
"events": {
"test_event_123": {
"status": "saved",
"message": "Test event",
"last_attempt": "2024-01-01T00:00:00",
"event": {
"properties": {
"label": "Test Event",
"what": "culture.geek"
}
}
}
},
"last_update": "2024-01-01T00:00:00"
}
scraper.events_data = test_data
scraper.save_events_data()
# Test de chargement
scraper2 = AgendaDuLibreScraper()
if "test_event_123" in scraper2.events_data["events"]:
print("✅ Persistance des données fonctionne correctement")
return True
else:
print("❌ Échec de la persistance des données")
return False
def main():
"""Exécute tous les tests"""
print("🚀 Démarrage des tests du scraper agenda du libre")
print("=" * 50)
tests = [
test_ical_fetch,
test_event_parsing,
test_data_persistence
]
passed = 0
total = len(tests)
for test in tests:
try:
if test():
passed += 1
print()
except Exception as e:
print(f"❌ Erreur lors du test {test.__name__}: {e}")
print()
print("=" * 50)
print(f"📊 Résultats: {passed}/{total} tests réussis")
if passed == total:
print("✅ Tous les tests sont passés!")
return True
else:
print("❌ Certains tests ont échoué")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

130
extractors/test_api_connection.py Executable file
View file

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""
Script de test de connexion à l'API OEDB pour le scraper agenda du libre
"""
import requests
import sys
import json
from datetime import datetime
# Configuration par défaut
api_oedb = "https://api.openeventdatabase.org"
def test_api_connection(api_url: str = api_oedb):
"""Test la connexion à l'API OEDB"""
print(f"🔍 Test de connexion à l'API OEDB: {api_url}")
print("=" * 50)
# Test 1: Endpoint de base
print("1⃣ Test de l'endpoint de base...")
try:
response = requests.get(f"{api_url}/", timeout=10)
if response.status_code == 200:
print("✅ Endpoint de base accessible")
else:
print(f"⚠️ Endpoint de base répond avec le code: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"❌ Erreur de connexion à l'endpoint de base: {e}")
return False
# Test 2: Endpoint des événements
print("\n2⃣ Test de l'endpoint des événements...")
try:
response = requests.get(f"{api_url}/events", timeout=10)
if response.status_code == 200:
print("✅ Endpoint des événements accessible")
try:
data = response.json()
if 'features' in data:
print(f" 📊 {len(data['features'])} événements trouvés dans l'API")
else:
print(" ⚠️ Format de réponse inattendu")
except json.JSONDecodeError:
print(" ⚠️ Réponse non-JSON reçue")
else:
print(f"❌ Endpoint des événements répond avec le code: {response.status_code}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Erreur de connexion à l'endpoint des événements: {e}")
return False
# Test 3: Test d'envoi d'un événement de test
print("\n3⃣ Test d'envoi d'un événement de test...")
test_event = {
"type": "Feature",
"properties": {
"label": f"Test API Connection - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"description": "Événement de test pour vérifier la connexion API",
"type": "scheduled",
"what": "community",
"where": "Test Location",
"start": datetime.now().isoformat(),
"stop": (datetime.now().timestamp() + 3600).isoformat(),
"source:name": "Test API Connection",
"last_modified_by": "test_script"
},
"geometry": {
"type": "Point",
"coordinates": [0, 0]
}
}
try:
response = requests.post(
f"{api_url}/event",
json=test_event,
headers={"Content-Type": "application/json"},
timeout=10
)
if response.status_code == 201:
print("✅ Événement de test créé avec succès")
try:
created_event = response.json()
event_id = created_event.get('id', 'inconnu')
print(f" 📝 ID de l'événement créé: {event_id}")
except json.JSONDecodeError:
print(" ⚠️ Réponse de création non-JSON")
elif response.status_code == 409:
print("⚠️ Événement de test déjà existant (conflit)")
else:
print(f"❌ Erreur lors de la création de l'événement de test: {response.status_code}")
print(f" Réponse: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Erreur lors de l'envoi de l'événement de test: {e}")
return False
print("\n✅ Tous les tests de connexion sont passés!")
return True
def main():
"""Fonction principale"""
import argparse
parser = argparse.ArgumentParser(description="Test de connexion à l'API OEDB")
parser.add_argument("--api-url", default=api_oedb,
help="URL de l'API OEDB à tester")
args = parser.parse_args()
success = test_api_connection(args.api_url)
if success:
print("\n🎉 L'API OEDB est prête pour le scraper agenda du libre!")
print("\n💡 Commandes utiles:")
print(" - Test complet: python3 test_agendadulibre.py")
print(" - Démonstration: python3 demo_agendadulibre.py")
print(" - Scraping réel: python3 agendadulibre.py --api-url " + args.api_url)
else:
print("\n❌ L'API OEDB n'est pas accessible ou ne fonctionne pas correctement.")
print("\n🔧 Vérifications à effectuer:")
print(" - L'API OEDB est-elle démarrée?")
print(" - L'URL est-elle correcte?")
print(" - Y a-t-il des erreurs dans les logs de l'API?")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View file

@ -15,8 +15,22 @@
<div class="content">
<header>
<h1>OEDB</h1>
<button class="login">login OSM</button>
<nav>
<a routerLink="/"><h1>
<img src="/static/oedb.png" alt="OEDB" style="width: 20px; height: 20px;">
OEDB
</h1>
</a>
<!-- <button class="login">login OSM</button> -->
<a routerLink="/agenda">agenda</a>
<a routerLink="/unlocated-events">événements non localisés</a>
<a routerLink="/nouvelles-categories">nouvelles catégories</a>
<a href="/demo/stats">stats</a>
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
</nav>
</header>
<main>
<router-outlet/>

View file

@ -1,16 +1,19 @@
<menu>
OpenEventDatabase
<nav>
<a routerLink="/agenda">agenda</a>
<a routerLink="/unlocated-events">événements non localisés</a>
<a href="/demo/stats">stats</a>
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
</nav>
(editor)
<div id="editor_form">
<div id="search_input">
<!-- <div id="search_input">
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
</div>
<!-- <div id="what_categories">
<div id="what_categories">
@for (oedbc of oedb_what_categories; track $index) {
<div class="category">
<div class="emoji">
@ -24,13 +27,13 @@
</div>
}
</div> -->
<!--
<hr>
(bouton de template pour ne pas remplir le formulaire)
<hr>
(reste optionnel du formulaire)
<!-- <label for="where">Nom</label>
<label for="where">Nom</label>
<input type="text" name="name">
<label for="where">Description</label>
@ -57,7 +60,7 @@
</div>
<div id="found_list">
<!-- <div id="found_list">
<h2>données</h2>
(liste des éléments trouvés)
<ul>
@ -87,6 +90,6 @@
<br>
points de l'utilisateur:
12 points.
</div>
</div> -->
</menu>

View file

@ -1,5 +1,17 @@
:host {
display: block;
nav{
a {
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.08);
display: block;
margin-bottom: 10px;
&:hover{
background-color: #f0f0f0;
}
}
}
}
#what_categories {

View file

@ -164,3 +164,23 @@ pre{
white-space: nowrap;
}
}
nav{
display: flex;
flex-direction: row;
gap: 10px;
padding: 10px;
a {
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.08);
display: inline-block;
margin-bottom: 10px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
&:hover{
background-color: #f0f0f0;
}
}
}

View file

@ -8,3 +8,4 @@ pytz==2025.2
Requests==2.32.5
waitress==3.0.2
jinja2
icalendar