Initial commit: Reorganiser le projet FFA Calendar Scraper

- Créer une arborescence propre (src/, scripts/, config/, data/, docs/, tests/)
- Déplacer les modules Python dans src/
- Déplacer les scripts autonomes dans scripts/
- Nettoyer les fichiers temporaires et __pycache__
- Mettre à jour le README.md avec documentation complète
- Mettre à jour les imports dans les scripts pour la nouvelle structure
- Configurer le .gitignore pour ignorer les données et logs
- Organiser les données dans data/ (courses, resultats, clubs, exports)

Structure du projet:
- src/: Modules principaux (ffa_scraper, ffa_analyzer)
- scripts/: Scripts CLI et utilitaires
- config/: Configuration (config.env)
- data/: Données générées
- docs/: Documentation
- tests/: Tests unitaires

💘 Generated with Crush

Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
Muyue
2026-01-01 18:05:14 +01:00
commit a5406a4e89
16 changed files with 3920 additions and 0 deletions

360
scripts/athlete_summary.py Executable file
View File

@@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""
Script pour générer un récapitulatif complet d'un athlète
Utilise les fichiers CSV pour extraire toutes les informations
"""
import pandas as pd
import os
import sys
import argparse
import logging
from datetime import datetime
from collections import defaultdict
import re
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def parse_time(time_str):
"""Convertir un temps en secondes pour comparaison"""
if not time_str or pd.isna(time_str):
return None
# Format HH:MM:SS
if ':' in time_str:
parts = time_str.split(':')
if len(parts) == 3:
try:
h, m, s = parts
return int(h) * 3600 + int(m) * 60 + float(s)
except:
pass
elif len(parts) == 2:
try:
m, s = parts
return int(m) * 60 + float(s)
except:
pass
# Format en secondes ou minutes
try:
# Enlever les non-numériques
clean = re.sub(r'[^\d.,]', '', str(time_str))
return float(clean.replace(',', '.'))
except:
return None
def format_time(seconds):
"""Formater des secondes en HH:MM:SS"""
if not seconds:
return "N/A"
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = seconds % 60
if hours > 0:
return f"{hours}:{minutes:02d}:{secs:05.2f}"
elif minutes > 0:
return f"{minutes}:{secs:05.2f}"
else:
return f"{secs:.2f}s"
def get_athlete_summary(nom, prenom=None, data_dir="data"):
"""Générer un récapitulatif complet d'un athlète"""
results_path = os.path.join(data_dir, 'resultats', 'results.csv')
courses_path = os.path.join(data_dir, 'courses', 'courses_list.csv')
if not os.path.exists(results_path):
logging.error(f"Fichier de résultats introuvable: {results_path}")
return None
try:
df_results = pd.read_csv(results_path, encoding='utf-8-sig')
# Filtre par nom
mask = df_results['nom'].str.contains(nom, case=False, na=False)
if prenom:
mask &= df_results['prenom'].str.contains(prenom, case=False, na=False)
results = df_results[mask]
if results.empty:
return None
# Charger les courses pour plus d'info
courses_df = None
if os.path.exists(courses_path):
courses_df = pd.read_csv(courses_path, encoding='utf-8-sig')
# Créer le récapitulatif
summary = {
'nom': results.iloc[0]['nom'],
'prenom': results.iloc[0]['prenom'],
'club': results.iloc[0]['club'],
'total_courses': len(results),
'categories': sorted(results['categorie'].dropna().unique().tolist()),
'results': [],
'statistics': {}
}
# Calculer les statistiques
places = []
times = []
podiums = 0
victoires = 0
courses_by_year = defaultdict(int)
courses_by_type = defaultdict(int)
for idx, result in results.iterrows():
# Extraire les places
try:
place = int(result['place'])
places.append(place)
if place == 1:
victoires += 1
podiums += 1
elif place <= 3:
podiums += 1
except:
pass
# Extraire les temps
time_seconds = parse_time(result.get('temps', result.get('resultat', '')))
if time_seconds:
times.append(time_seconds)
# Extraire les infos de course si disponible
course_info = {}
if courses_df is not None and 'course_url' in result:
course_match = courses_df[courses_df['lien'] == result['course_url']]
if not course_match.empty:
course_info = course_match.iloc[0].to_dict()
# Extraire l'année de la course
if 'date' in result and pd.notna(result['date']):
try:
year = str(result['date']).split('-')[0]
courses_by_year[year] += 1
except:
pass
elif course_info.get('date'):
try:
year = str(course_info['date']).split('-')[0]
courses_by_year[year] += 1
except:
pass
# Extraire le type de course
if course_info.get('type'):
courses_by_type[course_info['type']] += 1
# Ajouter le résultat détaillé
detailed_result = result.to_dict()
detailed_result['course_details'] = course_info
summary['results'].append(detailed_result)
# Calculer les statistiques finales
summary['statistics'] = {
'victoires': victoires,
'podiums': podiums,
'places_moyenne': sum(places) / len(places) if places else 0,
'meilleure_place': min(places) if places else None,
'pire_place': max(places) if places else None,
'meilleur_temps': min(times) if times else None,
'temps_moyen': sum(times) / len(times) if times else None,
'courses_par_annee': dict(courses_by_year),
'courses_par_type': dict(courses_by_type),
'categories': summary['categories']
}
return summary
except Exception as e:
logging.error(f"Erreur lors de la génération du récapitulatif: {e}")
return None
def display_athlete_summary(summary, show_full_results=False):
"""Afficher le récapitulatif de l'athlète"""
if not summary:
print("\n❌ Impossible de générer le récapitulatif")
return
stats = summary['statistics']
print(f"\n{'='*80}")
print(f"📊 RÉCAPITULATIF DE {summary['prenom']} {summary['nom']}")
print(f"{'='*80}\n")
# Informations générales
print(f"🏟️ Club: {summary['club']}")
print(f"🏃 Total des courses: {summary['total_courses']}")
print()
# Statistiques de performance
print(f"🏆 STATISTIQUES DE PERFORMANCE")
print(f"{''*40}")
print(f"Victoires: {stats['victoires']}")
print(f"Podiums (top 3): {stats['podiums']}")
print(f"Meilleure place: {stats['meilleure_place']}")
print(f"Place moyenne: {stats['places_moyenne']:.2f}")
print()
# Statistiques de temps
if stats['meilleur_temps']:
print(f"⏱️ STATISTIQUES DE TEMPS")
print(f"{''*40}")
print(f"Meilleur temps: {format_time(stats['meilleur_temps'])}")
print(f"Temps moyen: {format_time(stats['temps_moyen'])}")
print()
# Répartition par année
if stats['courses_par_annee']:
print(f"📅 RÉPARTITION PAR ANNÉE")
print(f"{''*40}")
for year, count in sorted(stats['courses_par_annee'].items()):
print(f"{year}: {count} course(s)")
print()
# Répartition par type
if stats['courses_par_type']:
print(f"🏷️ RÉPARTITION PAR TYPE DE COURSE")
print(f"{''*40}")
for course_type, count in sorted(stats['courses_par_type'].items(), key=lambda x: x[1], reverse=True):
print(f"{course_type}: {count} course(s)")
print()
# Catégories
if stats['categories']:
print(f"📊 CATÉGORIES")
print(f"{''*40}")
print(", ".join(cat for cat in stats['categories'] if cat))
print()
print(f"{'='*80}\n")
# Résultats détaillés
if show_full_results:
print(f"📋 LISTE COMPLÈTE DES RÉSULTATS")
print(f"{'='*80}\n")
for i, result in enumerate(summary['results'], 1):
print(f"{i}. {result.get('course_details', {}).get('nom', 'Inconnu')}")
if result.get('course_details', {}).get('date'):
print(f" 📅 {result['course_details']['date']}")
if result.get('course_details', {}).get('lieu'):
print(f" 📍 {result['course_details']['lieu']}")
print(f" 🏆 Place: {result.get('place', 'N/A')}")
temps = result.get('temps', result.get('resultat', 'N/A'))
print(f" ⏱️ Temps: {temps}")
if result.get('categorie'):
print(f" 🏷️ Catégorie: {result['categorie']}")
if result.get('points'):
print(f" 🎯 Points: {result['points']}")
print()
print(f"{'='*80}\n")
def export_summary_txt(summary, output_dir="data"):
"""Exporter le récapitulatif en fichier texte"""
os.makedirs(os.path.join(output_dir, 'exports'), exist_ok=True)
filename = f"summary_{summary['nom']}_{summary['prenom']}.txt"
filepath = os.path.join(output_dir, 'exports', filename.replace(" ", "_"))
with open(filepath, 'w', encoding='utf-8') as f:
f.write("="*80 + "\n")
f.write(f"RÉCAPITULATIF DE {summary['prenom']} {summary['nom']}\n")
f.write("="*80 + "\n\n")
stats = summary['statistics']
# Informations générales
f.write(f"Club: {summary['club']}\n")
f.write(f"Total des courses: {summary['total_courses']}\n\n")
# Statistiques
f.write("STATISTIQUES DE PERFORMANCE\n")
f.write("-"*40 + "\n")
f.write(f"Victoires: {stats['victoires']}\n")
f.write(f"Podiums: {stats['podiums']}\n")
f.write(f"Meilleure place: {stats['meilleure_place']}\n")
f.write(f"Place moyenne: {stats['places_moyenne']:.2f}\n\n")
# Temps
if stats['meilleur_temps']:
f.write("STATISTIQUES DE TEMPS\n")
f.write("-"*40 + "\n")
f.write(f"Meilleur temps: {format_time(stats['meilleur_temps'])}\n")
f.write(f"Temps moyen: {format_time(stats['temps_moyen'])}\n\n")
# Répartition
if stats['courses_par_annee']:
f.write("RÉPARTITION PAR ANNÉE\n")
f.write("-"*40 + "\n")
for year, count in sorted(stats['courses_par_annee'].items()):
f.write(f"{year}: {count}\n")
f.write("\n")
if stats['courses_par_type']:
f.write("RÉPARTITION PAR TYPE\n")
f.write("-"*40 + "\n")
for course_type, count in sorted(stats['courses_par_type'].items(), key=lambda x: x[1], reverse=True):
f.write(f"{course_type}: {count}\n")
f.write("\n")
# Liste des courses
f.write("LISTE DES COURSES\n")
f.write("="*80 + "\n\n")
for i, result in enumerate(summary['results'], 1):
f.write(f"{i}. {result.get('course_details', {}).get('nom', 'Inconnu')}\n")
f.write(f" Date: {result.get('course_details', {}).get('date', 'N/A')}\n")
f.write(f" Place: {result.get('place', 'N/A')}\n")
f.write(f" Temps: {result.get('temps', result.get('resultat', 'N/A'))}\n")
f.write("\n")
logging.info(f"Exporté le récapitulatif dans {filepath}")
return filepath
def main():
parser = argparse.ArgumentParser(description='Générer un récapitulatif complet d\'un athlète')
parser.add_argument('nom', help='Nom de l\'athlète')
parser.add_argument('--prenom', help='Prénom de l\'athlète')
parser.add_argument('--data-dir', default='data',
help='Répertoire des données CSV')
parser.add_argument('--full', action='store_true',
help='Afficher la liste complète des résultats')
parser.add_argument('--export', action='store_true',
help='Exporter le récapitulatif en fichier texte')
args = parser.parse_args()
# Générer le récapitulatif
print(f"\n📊 Génération du récapitulatif pour {args.prenom or ''} {args.nom}...")
summary = get_athlete_summary(args.nom, args.prenom, args.data_dir)
if summary:
display_athlete_summary(summary, show_full_results=args.full)
if args.export:
filepath = export_summary_txt(summary, args.data_dir)
print(f"💾 Exporté dans: {filepath}")
else:
print("\n❌ Aucun résultat trouvé pour cet athlète")
print("💡 Vérifiez que les données ont été scrapées avec:")
print(" python ffa_cli.py scrape --fetch-details")
if __name__ == "__main__":
main()