536 lines
No EOL
20 KiB
Python
536 lines
No EOL
20 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script de démonstration des fonctionnalités de la classe EpisodePollution
|
|
=====================================================================
|
|
|
|
Ce script illustre l'utilisation de toutes les méthodes disponibles
|
|
dans la classe EpisodePollution pour analyser les épisodes de pollution.
|
|
|
|
Fonctionnalités testées:
|
|
- Analyse des alertes de pollution
|
|
- Identification des polluants responsables
|
|
- Analyse des niveaux d'alerte (Information/Alerte)
|
|
- Gestion des géométries complexes
|
|
- Analyse des zones géographiques affectées
|
|
- Utilisation des méthodes héritées de la classe de base
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from atmo_wrapper import AtmoDataWrapper
|
|
from atmo_data_wrapper import EpisodePollution, AtmoDataCollection, Coordinates
|
|
from atmo_data_wrapper import CODE_POLLUANT
|
|
|
|
def demo_basic_properties():
|
|
"""Démonstration des propriétés de base d'un objet EpisodePollution"""
|
|
print("=" * 60)
|
|
print("DÉMONSTRATION DES PROPRIÉTÉS DE BASE")
|
|
print("=" * 60)
|
|
|
|
# Exemple d'épisode de pollution aux particules fines
|
|
sample_episode = {
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "MultiPolygon",
|
|
"coordinates": [[[
|
|
[6.0, 49.0], [6.5, 49.0], [6.5, 49.5], [6.0, 49.5], [6.0, 49.0]
|
|
]]]
|
|
},
|
|
"properties": {
|
|
"aasqa": "90",
|
|
"source": "ATMO Grand Est",
|
|
"date_maj": "2024-01-15T10:00:00",
|
|
"lib_zone": "Agglomération de Nancy",
|
|
"code_pol": "5",
|
|
"lib_pol": "PM10",
|
|
"code_zone": "54395",
|
|
"date_dif": "2024-01-15",
|
|
"date_ech": "2024-01-16",
|
|
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION"
|
|
}
|
|
}
|
|
|
|
episode = EpisodePollution(sample_episode)
|
|
|
|
print(f"Zone affectée: {episode.lib_zone}")
|
|
print(f"Code polluant: {episode.code_pol}")
|
|
print(f"Polluant: {episode.lib_pol}")
|
|
print(f"Code polluant normalisé: {episode.get_polluant_code()}")
|
|
print(f"État: {episode.etat}")
|
|
print(f"AASQA: {episode.get_aasqa_name()}")
|
|
print(f"Source: {episode.get_source()}")
|
|
print(f"Date de diffusion: {episode.date_dif}")
|
|
print(f"Date d'échéance: {episode.date_ech}")
|
|
print(f"Date de mise à jour: {episode.date_maj}")
|
|
print(f"Géométrie complexe: {episode.is_geometry_complex()}")
|
|
|
|
if episode.has_coordinates():
|
|
print(f"Coordonnées: {episode.coordinates}")
|
|
else:
|
|
print("Pas de coordonnées ponctuelles (géométrie de zone)")
|
|
|
|
print(f"Représentation: {episode}")
|
|
print()
|
|
|
|
def demo_alert_analysis():
|
|
"""Démonstration de l'analyse des alertes"""
|
|
print("=" * 60)
|
|
print("ANALYSE DES ALERTES DE POLLUTION")
|
|
print("=" * 60)
|
|
|
|
# Exemples d'épisodes avec différents niveaux d'alerte
|
|
episodes_samples = [
|
|
{
|
|
"properties": {
|
|
"aasqa": "18", "lib_zone": "Île-de-France", "code_pol": "5", "lib_pol": "PM10",
|
|
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION", "date_dif": "2024-01-15"
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
"aasqa": "18", "lib_zone": "Paris", "code_pol": "3", "lib_pol": "O3",
|
|
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-07-20"
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
"aasqa": "90", "lib_zone": "Strasbourg", "code_pol": "1", "lib_pol": "NO2",
|
|
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-01-15"
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
"aasqa": "13", "lib_zone": "Marseille", "code_pol": "6", "lib_pol": "PM2.5",
|
|
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-03-10"
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
"aasqa": "84", "lib_zone": "Lyon", "code_pol": "3", "lib_pol": "O3",
|
|
"etat": "ALERTE NIVEAU 1", "date_dif": "2024-08-15"
|
|
}
|
|
}
|
|
]
|
|
|
|
print("Analyse des différents types d'alertes:")
|
|
print()
|
|
|
|
for i, episode_data in enumerate(episodes_samples, 1):
|
|
episode_data["type"] = "Feature"
|
|
episode = EpisodePollution(episode_data)
|
|
|
|
print(f"{i}. {episode.lib_zone} - {episode.lib_pol}")
|
|
print(f" État: {episode.etat}")
|
|
print(f" Alerte active: {'✓' if episode.is_alert_active() else '✗'}")
|
|
print(f" Niveau d'alerte: {episode.get_alert_level()}")
|
|
print(f" Code polluant: {episode.get_polluant_code()}")
|
|
print(f" Date: {episode.date_dif}")
|
|
print()
|
|
|
|
def demo_pollutant_analysis():
|
|
"""Démonstration de l'analyse par polluant"""
|
|
print("=" * 60)
|
|
print("ANALYSE PAR POLLUANT")
|
|
print("=" * 60)
|
|
|
|
# Exemples avec différents polluants
|
|
pollutants_samples = [
|
|
{"code_pol": "1", "lib_pol": "NO2", "zone": "Zone urbaine dense"},
|
|
{"code_pol": "2", "lib_pol": "SO2", "zone": "Zone industrielle"},
|
|
{"code_pol": "3", "lib_pol": "O3", "zone": "Zone péri-urbaine"},
|
|
{"code_pol": "5", "lib_pol": "PM10", "zone": "Centre-ville"},
|
|
{"code_pol": "6", "lib_pol": "PM2.5", "zone": "Zone trafic"}
|
|
]
|
|
|
|
print("Polluants détectés dans les épisodes:")
|
|
print()
|
|
|
|
for i, poll_data in enumerate(pollutants_samples, 1):
|
|
episode_data = {
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "99", "lib_zone": poll_data["zone"],
|
|
"code_pol": poll_data["code_pol"], "lib_pol": poll_data["lib_pol"],
|
|
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
|
|
}
|
|
}
|
|
|
|
episode = EpisodePollution(episode_data)
|
|
polluant_description = CODE_POLLUANT.get(episode.get_polluant_code(), "Description non disponible")
|
|
|
|
print(f"{i}. Polluant: {episode.lib_pol} (Code: {episode.code_pol})")
|
|
print(f" Code normalisé: {episode.get_polluant_code()}")
|
|
print(f" Zone: {episode.lib_zone}")
|
|
print(f" Description: {polluant_description}")
|
|
print()
|
|
|
|
def demo_geometry_analysis():
|
|
"""Démonstration de l'analyse des géométries"""
|
|
print("=" * 60)
|
|
print("ANALYSE DES GÉOMÉTRIES")
|
|
print("=" * 60)
|
|
|
|
# Épisode avec géométrie simple (Point)
|
|
episode_point = EpisodePollution({
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [2.3522, 48.8566]
|
|
},
|
|
"properties": {
|
|
"aasqa": "18", "lib_zone": "Paris Centre", "code_pol": "3", "lib_pol": "O3",
|
|
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-07-15"
|
|
}
|
|
})
|
|
|
|
# Épisode avec géométrie complexe (MultiPolygon)
|
|
episode_multipolygon = EpisodePollution({
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "MultiPolygon",
|
|
"coordinates": [
|
|
[[[2.0, 48.5], [2.5, 48.5], [2.5, 49.0], [2.0, 49.0], [2.0, 48.5]]],
|
|
[[[2.8, 48.7], [3.2, 48.7], [3.2, 49.1], [2.8, 49.1], [2.8, 48.7]]]
|
|
]
|
|
},
|
|
"properties": {
|
|
"aasqa": "18", "lib_zone": "Île-de-France", "code_pol": "5", "lib_pol": "PM10",
|
|
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-01-20"
|
|
}
|
|
})
|
|
|
|
# Épisode sans géométrie
|
|
episode_no_geom = EpisodePollution({
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "90", "lib_zone": "Grand Est", "code_pol": "1", "lib_pol": "NO2",
|
|
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-01-15"
|
|
}
|
|
})
|
|
|
|
episodes = [
|
|
("Point", episode_point),
|
|
("MultiPolygon", episode_multipolygon),
|
|
("Sans géométrie", episode_no_geom)
|
|
]
|
|
|
|
print("Types de géométries dans les épisodes:")
|
|
print()
|
|
|
|
for i, (type_geom, episode) in enumerate(episodes, 1):
|
|
print(f"{i}. {type_geom} - {episode.lib_zone}")
|
|
print(f" Type géométrie: {episode.geometry.get('type', 'Non défini')}")
|
|
print(f" Géométrie complexe: {'✓' if episode.is_geometry_complex() else '✗'}")
|
|
print(f" A des coordonnées: {'✓' if episode.has_coordinates() else '✗'}")
|
|
if episode.has_coordinates():
|
|
print(f" Coordonnées: {episode.coordinates}")
|
|
print(f" État: {episode.etat}")
|
|
print()
|
|
|
|
def demo_temporal_analysis():
|
|
"""Démonstration de l'analyse temporelle des épisodes"""
|
|
print("=" * 60)
|
|
print("ANALYSE TEMPORELLE DES ÉPISODES")
|
|
print("=" * 60)
|
|
|
|
# Simulation d'épisodes à différentes dates
|
|
temporal_episodes = [
|
|
{
|
|
"date_dif": "2024-01-15", "date_ech": "2024-01-16",
|
|
"lib_zone": "Lyon", "lib_pol": "PM10", "etat": "PROCEDURE D'INFORMATION",
|
|
"aasqa": "84", "code_pol": "5"
|
|
},
|
|
{
|
|
"date_dif": "2024-07-20", "date_ech": "2024-07-21",
|
|
"lib_zone": "Marseille", "lib_pol": "O3", "etat": "PROCEDURE D'ALERTE",
|
|
"aasqa": "13", "code_pol": "3"
|
|
},
|
|
{
|
|
"date_dif": "2024-12-05", "date_ech": "2024-12-07",
|
|
"lib_zone": "Strasbourg", "lib_pol": "NO2", "etat": "PROCEDURE D'INFORMATION",
|
|
"aasqa": "90", "code_pol": "1"
|
|
}
|
|
]
|
|
|
|
print("Épisodes chronologiques:")
|
|
print()
|
|
|
|
for i, episode_data in enumerate(temporal_episodes, 1):
|
|
episode_data["type"] = "Feature"
|
|
episode = EpisodePollution(episode_data)
|
|
|
|
# Calcul de la durée (simplifié)
|
|
try:
|
|
from datetime import datetime
|
|
date_debut = datetime.strptime(episode.date_dif, "%Y-%m-%d")
|
|
date_fin = datetime.strptime(episode.date_ech, "%Y-%m-%d")
|
|
duree = (date_fin - date_debut).days
|
|
except:
|
|
duree = "Non calculable"
|
|
|
|
print(f"{i}. {episode.lib_zone} - {episode.lib_pol}")
|
|
print(f" Période: du {episode.date_dif} au {episode.date_ech}")
|
|
print(f" Durée: {duree} jour(s)" if duree != "Non calculable" else f" Durée: {duree}")
|
|
print(f" État: {episode.etat}")
|
|
print(f" Niveau: {episode.get_alert_level()}")
|
|
print()
|
|
|
|
def demo_regional_analysis():
|
|
"""Démonstration de l'analyse régionale des épisodes"""
|
|
print("=" * 60)
|
|
print("ANALYSE RÉGIONALE DES ÉPISODES")
|
|
print("=" * 60)
|
|
|
|
# Simulation d'épisodes dans différentes régions
|
|
regional_episodes = [
|
|
{"aasqa": "18", "region": "Île-de-France", "lib_zone": "Paris", "lib_pol": "PM10"},
|
|
{"aasqa": "84", "region": "Auvergne-Rhône-Alpes", "lib_zone": "Lyon", "lib_pol": "O3"},
|
|
{"aasqa": "13", "region": "Provence-Alpes-Côte d'Azur", "lib_zone": "Marseille", "lib_pol": "PM2.5"},
|
|
{"aasqa": "90", "region": "Grand Est", "lib_zone": "Strasbourg", "lib_pol": "NO2"},
|
|
{"aasqa": "59", "region": "Hauts-de-France", "lib_zone": "Lille", "lib_pol": "O3"}
|
|
]
|
|
|
|
print("Épisodes par région AASQA:")
|
|
print()
|
|
|
|
for i, episode_data in enumerate(regional_episodes, 1):
|
|
episode_data.update({
|
|
"type": "Feature",
|
|
"code_pol": "5", "etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
|
|
})
|
|
|
|
episode = EpisodePollution(episode_data)
|
|
|
|
print(f"{i}. Région: {episode_data['region']}")
|
|
print(f" AASQA: {episode.get_aasqa_name()}")
|
|
print(f" Zone: {episode.lib_zone}")
|
|
print(f" Polluant: {episode.lib_pol}")
|
|
print(f" Code AASQA: {episode.aasqa}")
|
|
print()
|
|
|
|
def demo_inherited_methods():
|
|
"""Démonstration des méthodes héritées de la classe de base"""
|
|
print("=" * 60)
|
|
print("MÉTHODES HÉRITÉES DE LA CLASSE DE BASE")
|
|
print("=" * 60)
|
|
|
|
episode_data = {
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [4.8357, 45.7640]
|
|
},
|
|
"properties": {
|
|
"aasqa": "84",
|
|
"source": "ATMO Auvergne-Rhône-Alpes",
|
|
"date_maj": "2024-07-15T14:30:00",
|
|
"lib_zone": "Lyon Métropole",
|
|
"code_pol": "3",
|
|
"lib_pol": "O3",
|
|
"etat": "PROCEDURE D'ALERTE",
|
|
"date_dif": "2024-07-15",
|
|
"date_ech": "2024-07-16"
|
|
}
|
|
}
|
|
|
|
episode = EpisodePollution(episode_data)
|
|
|
|
print(f"Zone affectée: {episode.lib_zone}")
|
|
print(f"AASQA: {episode.get_aasqa_name()}")
|
|
print(f"Source: {episode.get_source()}")
|
|
print(f"A des coordonnées: {'✓' if episode.has_coordinates() else '✗'}")
|
|
if episode.has_coordinates():
|
|
print(f"Coordonnées: {episode.coordinates}")
|
|
print()
|
|
|
|
# Test des fonctions de couleur et emoji (niveau fictif pour démonstration)
|
|
print("Fonctions de couleur et emoji (exemple avec niveau 4 - Alerte):")
|
|
test_level = 4 # Niveau d'alerte
|
|
couleur_hex, couleur_rgb = episode.get_color_by_level(test_level)
|
|
emoji_round = episode.get_emoji_by_level(test_level, "round")
|
|
emoji_square = episode.get_emoji_by_level(test_level, "square")
|
|
|
|
print(f" - Couleur hex: {couleur_hex}")
|
|
print(f" - Couleur RGB: {couleur_rgb}")
|
|
print(f" - Emoji rond: {emoji_round}")
|
|
print(f" - Emoji carré: {emoji_square}")
|
|
print()
|
|
|
|
def demo_comparative_analysis():
|
|
"""Démonstration d'une analyse comparative entre épisodes"""
|
|
print("=" * 60)
|
|
print("ANALYSE COMPARATIVE DES ÉPISODES")
|
|
print("=" * 60)
|
|
|
|
episodes_data = [
|
|
{
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "18", "lib_zone": "Paris", "code_pol": "5", "lib_pol": "PM10",
|
|
"etat": "PROCEDURE D'INFORMATION ET DE RECOMMANDATION", "date_dif": "2024-01-15"
|
|
}
|
|
},
|
|
{
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "84", "lib_zone": "Lyon", "code_pol": "3", "lib_pol": "O3",
|
|
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-07-20"
|
|
}
|
|
},
|
|
{
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "13", "lib_zone": "Marseille", "code_pol": "6", "lib_pol": "PM2.5",
|
|
"etat": "ALERTE NIVEAU 1", "date_dif": "2024-03-10"
|
|
}
|
|
},
|
|
{
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "90", "lib_zone": "Strasbourg", "code_pol": "1", "lib_pol": "NO2",
|
|
"etat": "PAS DE DEPASSEMENT", "date_dif": "2024-05-05"
|
|
}
|
|
}
|
|
]
|
|
|
|
episodes = [EpisodePollution(ep) for ep in episodes_data]
|
|
|
|
print("Comparaison des épisodes de pollution:")
|
|
print("-" * 70)
|
|
print(f"{'Zone':<15} {'Polluant':<8} {'Niveau':<12} {'Alerte':<8} {'État'}")
|
|
print("-" * 70)
|
|
|
|
for episode in episodes:
|
|
alerte_status = "✓" if episode.is_alert_active() else "✗"
|
|
niveau = episode.get_alert_level()
|
|
|
|
print(f"{episode.lib_zone:<15} {episode.lib_pol:<8} {niveau:<12} {alerte_status:<8} {episode.etat}")
|
|
|
|
print()
|
|
|
|
# Statistiques
|
|
total_episodes = len(episodes)
|
|
alertes_actives = sum(1 for ep in episodes if ep.is_alert_active())
|
|
|
|
print(f"Statistiques:")
|
|
print(f" - Total d'épisodes: {total_episodes}")
|
|
print(f" - Alertes actives: {alertes_actives}")
|
|
print(f" - Pourcentage d'alertes: {(alertes_actives/total_episodes)*100:.1f}%")
|
|
|
|
# Répartition par polluant
|
|
polluants = {}
|
|
for episode in episodes:
|
|
polluant = episode.get_polluant_code()
|
|
polluants[polluant] = polluants.get(polluant, 0) + 1
|
|
|
|
print(f" - Répartition par polluant:")
|
|
for polluant, count in polluants.items():
|
|
print(f" * {polluant}: {count} épisode(s)")
|
|
|
|
print()
|
|
|
|
def demo_edge_cases():
|
|
"""Démonstration de la gestion des cas particuliers"""
|
|
print("=" * 60)
|
|
print("GESTION DES CAS PARTICULIERS")
|
|
print("=" * 60)
|
|
|
|
# Cas 1: Épisode sans état défini
|
|
print("1. Épisode sans état défini:")
|
|
episode_no_state = EpisodePollution({
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "99", "lib_zone": "Zone test", "code_pol": "5", "lib_pol": "PM10",
|
|
"etat": "", "date_dif": "2024-01-15"
|
|
}
|
|
})
|
|
|
|
print(f" État: '{episode_no_state.etat}'")
|
|
print(f" Alerte active: {'✓' if episode_no_state.is_alert_active() else '✗'}")
|
|
print(f" Niveau d'alerte: {episode_no_state.get_alert_level()}")
|
|
print()
|
|
|
|
# Cas 2: Code polluant non standard
|
|
print("2. Code polluant non référencé:")
|
|
episode_unknown_pollutant = EpisodePollution({
|
|
"type": "Feature",
|
|
"properties": {
|
|
"aasqa": "99", "lib_zone": "Zone test", "code_pol": "99", "lib_pol": "Polluant inconnu",
|
|
"etat": "PROCEDURE D'INFORMATION", "date_dif": "2024-01-15"
|
|
}
|
|
})
|
|
|
|
print(f" Code polluant original: {episode_unknown_pollutant.code_pol}")
|
|
print(f" Code polluant normalisé: {episode_unknown_pollutant.get_polluant_code()}")
|
|
print(f" Nom polluant: {episode_unknown_pollutant.lib_pol}")
|
|
print()
|
|
|
|
# Cas 3: Géométrie malformée
|
|
print("3. Géométrie non standard:")
|
|
episode_no_geometry = EpisodePollution({
|
|
"type": "Feature",
|
|
"geometry": None,
|
|
"properties": {
|
|
"aasqa": "99", "lib_zone": "Zone sans géométrie", "code_pol": "3", "lib_pol": "O3",
|
|
"etat": "PROCEDURE D'ALERTE", "date_dif": "2024-01-15"
|
|
}
|
|
})
|
|
|
|
print(f" Géométrie: {episode_no_geometry.geometry}")
|
|
print(f" Type géométrie: {episode_no_geometry.geometry.get('type', 'Non défini') if episode_no_geometry.geometry else 'None'}")
|
|
print(f" Géométrie complexe: {'✓' if episode_no_geometry.is_geometry_complex() else '✗'}")
|
|
print(f" A des coordonnées: {'✓' if episode_no_geometry.has_coordinates() else '✗'}")
|
|
print()
|
|
|
|
def main():
|
|
"""Fonction principale de démonstration"""
|
|
print("SCRIPT DE DÉMONSTRATION - CLASSE EPISODEPOLLUTION")
|
|
print("=" * 60)
|
|
print("Ce script teste toutes les fonctionnalités de la classe EpisodePollution")
|
|
print("pour l'analyse des épisodes de pollution atmosphérique.")
|
|
print()
|
|
|
|
try:
|
|
# Exécution de toutes les démonstrations
|
|
demo_basic_properties()
|
|
demo_alert_analysis()
|
|
demo_pollutant_analysis()
|
|
demo_geometry_analysis()
|
|
demo_temporal_analysis()
|
|
demo_regional_analysis()
|
|
demo_inherited_methods()
|
|
demo_comparative_analysis()
|
|
demo_edge_cases()
|
|
|
|
print("=" * 60)
|
|
print("RÉCAPITULATIF DES MÉTHODES TESTÉES")
|
|
print("=" * 60)
|
|
print("Méthodes spécifiques à EpisodePollution:")
|
|
print("✓ get_polluant_code()")
|
|
print("✓ is_alert_active()")
|
|
print("✓ get_alert_level()")
|
|
print("✓ is_geometry_complex()")
|
|
print()
|
|
print("Méthodes héritées de AtmoDataBase:")
|
|
print("✓ get_aasqa_name()")
|
|
print("✓ get_source()")
|
|
print("✓ has_coordinates()")
|
|
print("✓ get_emoji_by_level(level, style)")
|
|
print("✓ get_color_by_level(level)")
|
|
print()
|
|
print("Propriétés testées:")
|
|
print("✓ Codes et noms des polluants")
|
|
print("✓ États et niveaux d'alerte")
|
|
print("✓ Zones géographiques affectées")
|
|
print("✓ Géométries (Point, MultiPolygon)")
|
|
print("✓ Informations temporelles (dates)")
|
|
print("✓ Gestion des cas particuliers")
|
|
print()
|
|
print("✅ TOUTES LES FONCTIONNALITÉS ONT ÉTÉ TESTÉES AVEC SUCCÈS")
|
|
|
|
except Exception as e:
|
|
print(f"❌ ERREUR lors de l'exécution: {e}")
|
|
raise
|
|
|
|
if __name__ == "__main__":
|
|
main() |