# Fichier: python_cheats/cheatsheets/schedule.txt
# Cheatsheet Schedule Python - Guide Complet
# Bibliothèque pour planifier des tâches périodiques


[OK] QU'EST-CE QUE SCHEDULE ?

# Schedule est une bibliothèque Python simple et élégante pour planifier 
# l'exécution de fonctions (jobs) à des intervalles réguliers ou à des moments précis.

# Pensez à schedule comme un "cron" Python mais plus simple :
# - Cron = outil système avec syntaxe cryptique (*/5 * * * *)
# - Schedule = code Python lisible (every(5).minutes.do(job))

# QUAND UTILISER SCHEDULE ?
# [OK] Scripts qui tournent en continu et doivent répéter des tâches
# [OK] Tâches de maintenance (backups, nettoyage, monitoring)
# [OK] Synchronisation de données périodique
# [OK] Envoi de rapports automatiques
# [OK] Web scraping régulier
# [OK] Vérifications de santé système

# QUAND NE PAS UTILISER SCHEDULE ?
# [X] Tâches système critiques (utiliser cron)
# [X] Applications distribuées (utiliser Celery Beat)
# [X] Workflows complexes avec dépendances (utiliser Airflow)
# [X] Si vous voulez que la tâche survive au redémarrage (utiliser cron/systemd)


[OK] INSTALLATION

# Installer schedule
pip install schedule

# Vérifier installation
pip show schedule

# Version recommandée
pip install schedule==1.2.0

# Dans requirements.txt
schedule==1.2.0

# Importer dans votre code
import schedule
import time


[OK] CONCEPTS FONDAMENTAUX (POUR DÉBUTANTS)

# ═══════════════════════════════════════════════════════════════
# CONCEPT 1: LA FONCTION (JOB)
# ═══════════════════════════════════════════════════════════════

# Un "job" est simplement une fonction Python que vous voulez exécuter
def ma_tache():
    print("Cette fonction s'exécute automatiquement")
    # Vous pouvez y mettre n'importe quel code Python

# ═══════════════════════════════════════════════════════════════
# CONCEPT 2: LA PLANIFICATION
# ═══════════════════════════════════════════════════════════════

# Vous "planifiez" quand la fonction doit s'exécuter
schedule.every(10).seconds.do(ma_tache)
#       ^         ^        ^      ^
#    "chaque"  "10"   "secondes" "exécuter ma_tache"

# Cela signifie : "Exécute ma_tache toutes les 10 secondes"

# ═══════════════════════════════════════════════════════════════
# CONCEPT 3: LE LOOP (BOUCLE PRINCIPALE)
# ═══════════════════════════════════════════════════════════════

# Schedule NE s'exécute PAS tout seul automatiquement !
# Vous devez créer une boucle qui vérifie continuellement s'il faut lancer une tâche

while True:                      # Boucle infinie
    schedule.run_pending()       # "Y a-t-il une tâche à lancer maintenant ?"
    time.sleep(1)                # Attendre 1 seconde avant de revérifier

# POURQUOI CE LOOP EST NÉCESSAIRE ?
# - schedule.run_pending() vérifie si une tâche doit s'exécuter MAINTENANT
# - time.sleep(1) évite que votre programme consomme 100% du CPU
# - Sans ce loop, rien ne se passerait car schedule ne vérifie jamais !

# ═══════════════════════════════════════════════════════════════
# EXEMPLE COMPLET MINIMAL (VOTRE PREMIER PROGRAMME)
# ═══════════════════════════════════════════════════════════════

import schedule
import time
from datetime import datetime

def dire_bonjour():
    """Cette fonction affiche l'heure actuelle"""
    maintenant = datetime.now().strftime("%H:%M:%S")
    print(f"Bonjour ! Il est {maintenant}")

# Planifier : exécuter dire_bonjour toutes les 5 secondes
schedule.every(5).seconds.do(dire_bonjour)

print("Programme démarré. Appuyez Ctrl+C pour arrêter.")

# Boucle principale
while True:
    schedule.run_pending()  # Vérifie et exécute les tâches en attente
    time.sleep(1)           # Pause de 1 seconde

# TESTEZ CE CODE :
# 1. Copiez-le dans un fichier test.py
# 2. Exécutez : python test.py
# 3. Vous verrez "Bonjour ! Il est HH:MM:SS" toutes les 5 secondes
# 4. Arrêtez avec Ctrl+C


[OK] COMPRENDRE LA SYNTAXE (EXPLICATION DÉTAILLÉE)

# ═══════════════════════════════════════════════════════════════
# ANATOMIE D'UNE PLANIFICATION
# ═══════════════════════════════════════════════════════════════

schedule.every(10).seconds.do(ma_fonction)
#   │      │       │       │       │
#   │      │       │       │       └─ Fonction à exécuter (sans parenthèses!)
#   │      │       │       └─ Méthode qui enregistre la tâche
#   │      │       └─ Unité de temps (seconds, minutes, hours, days)
#   │      └─ Nombre (intervalle)
#   └─ Module schedule

# RÈGLE IMPORTANTE : Pas de parenthèses () après la fonction !
schedule.every(10).seconds.do(ma_fonction)     # [OK] CORRECT
schedule.every(10).seconds.do(ma_fonction())   # [X] ERREUR !
#                                          ^^
# Les parenthèses exécutent la fonction immédiatement au lieu de la planifier

# ═══════════════════════════════════════════════════════════════
# VARIATIONS DE SYNTAXE
# ═══════════════════════════════════════════════════════════════

# Intervalle explicite
schedule.every(10).seconds.do(job)     # Toutes les 10 secondes
schedule.every(5).minutes.do(job)      # Toutes les 5 minutes
schedule.every(2).hours.do(job)        # Toutes les 2 heures

# Sans nombre = intervalle de 1
schedule.every().second.do(job)        # Chaque seconde (= every(1).second)
schedule.every().minute.do(job)        # Chaque minute
schedule.every().hour.do(job)          # Chaque heure
schedule.every().day.do(job)           # Chaque jour

# Singulier vs Pluriel (les deux fonctionnent)
schedule.every(1).second.do(job)       # Singulier
schedule.every(1).seconds.do(job)      # Pluriel - identique !

# ═══════════════════════════════════════════════════════════════
# HEURES PRÉCISES
# ═══════════════════════════════════════════════════════════════

# Format : "HH:MM" ou "HH:MM:SS" (format 24 heures)
schedule.every().day.at("10:30").do(job)       # Chaque jour à 10h30
schedule.every().day.at("14:00").do(job)       # Chaque jour à 14h00
schedule.every().day.at("09:15:30").do(job)    # Avec secondes

# ATTENTION : at() fonctionne avec day, monday, tuesday, etc.
schedule.every().day.at("10:30").do(job)       # [OK] CORRECT
schedule.every(5).hours.at("10:30").do(job)    # [X] ERREUR ! (pas de at avec hours)

# Heures dans l'heure
schedule.every().hour.at(":17").do(job)        # Chaque heure à 17 minutes
#                           ^^
#                  Notez le : avant le nombre

schedule.every().minute.at(":30").do(job)      # Chaque minute à 30 secondes


[OK] EXEMPLES PRATIQUES POUR BIEN COMPRENDRE

# ═══════════════════════════════════════════════════════════════
# EXEMPLE 1 : Sauvegarder un fichier toutes les 30 secondes
# ═══════════════════════════════════════════════════════════════

import schedule
import time
from datetime import datetime

def sauvegarder_donnees():
    """Simule une sauvegarde de données"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    # Dans la vraie vie, vous sauvegarderiez dans un fichier ou BDD
    with open("auto_save.txt", "a") as f:
        f.write(f"Sauvegarde effectuée à {timestamp}\n")
    
    print(f"[OK] Données sauvegardées à {timestamp}")

# Planifier la sauvegarde
schedule.every(30).seconds.do(sauvegarder_donnees)

print("Système de sauvegarde automatique démarré...")
print("Les données seront sauvegardées toutes les 30 secondes")

while True:
    schedule.run_pending()
    time.sleep(1)

# ═══════════════════════════════════════════════════════════════
# EXEMPLE 2 : Rappel de pause toutes les heures (pour développeurs)
# ═══════════════════════════════════════════════════════════════

def rappel_pause():
    """Rappelle au développeur de faire une pause"""
    print("\n" + "="*50)
    print("[ALARM_CLOCK] PAUSE RECOMMANDÉE !")
    print("Levez-vous, étirez-vous, regardez au loin")
    print("Vos yeux et votre dos vous remercieront [SMILING_FACE_WITH_SMILING_EYES]")
    print("="*50 + "\n")

# Rappel toutes les heures
schedule.every().hour.do(rappel_pause)

print("Rappels de pause activés (toutes les heures)")

while True:
    schedule.run_pending()
    time.sleep(1)

# ═══════════════════════════════════════════════════════════════
# EXEMPLE 3 : Vérifier un site web toutes les 5 minutes
# ═══════════════════════════════════════════════════════════════

import requests

def verifier_site():
    """Vérifie si un site web est accessible"""
    url = "https://www.example.com"
    
    try:
        response = requests.get(url, timeout=5)
        if response.status_code == 200:
            print(f"[OK] {url} est accessible (code {response.status_code})")
        else:
            print(f"[ATTENTION] {url} retourne le code {response.status_code}")
    except requests.exceptions.RequestException as e:
        print(f"[X] {url} est inaccessible : {e}")

# Vérifier toutes les 5 minutes
schedule.every(5).minutes.do(verifier_site)

# Aussi vérifier immédiatement au démarrage
verifier_site()

print("Monitoring du site démarré (vérification toutes les 5 minutes)")

while True:
    schedule.run_pending()
    time.sleep(1)


[OK] PASSER DES PARAMÈTRES AUX FONCTIONS (IMPORTANT!)

# ═══════════════════════════════════════════════════════════════
# PROBLÈME : Comment passer des arguments à une fonction planifiée ?
# ═══════════════════════════════════════════════════════════════

# Fonction qui nécessite des paramètres
def envoyer_email(destinataire, sujet):
    print(f"Email envoyé à {destinataire}")
    print(f"Sujet: {sujet}")

# MÉTHODE 1 : Passer les arguments directement après la fonction
schedule.every(10).seconds.do(envoyer_email, "alice@example.com", "Rapport quotidien")
#                                           ^                      ^
#                                    argument 1              argument 2

# MÉTHODE 2 : Avec arguments nommés (kwargs)
schedule.every(10).seconds.do(
    envoyer_email, 
    destinataire="bob@example.com",
    sujet="Alerte système"
)

# MÉTHODE 3 : Utiliser une lambda (pour cas simples)
schedule.every(10).seconds.do(lambda: envoyer_email("charlie@example.com", "Test"))

# MÉTHODE 4 : Créer une fonction wrapper
def job_envoyer_email():
    envoyer_email("david@example.com", "Backup terminé")

schedule.every().day.at("09:00").do(job_envoyer_email)

# EXEMPLE COMPLET AVEC PARAMÈTRES
def sauvegarder_fichier(nom_fichier, format_compression="zip"):
    """Sauvegarde avec paramètres personnalisables"""
    print(f"Sauvegarde de {nom_fichier} au format {format_compression}")
    # Logique de sauvegarde...

# Différentes sauvegardes avec paramètres différents
schedule.every().day.at("02:00").do(sauvegarder_fichier, "base_donnees.sql", "gzip")
schedule.every().hour.do(sauvegarder_fichier, "logs.txt", format_compression="zip")
schedule.every(30).minutes.do(sauvegarder_fichier, nom_fichier="cache.json")


[OK] COMPRENDRE run_pending() ET LE LOOP

# ═══════════════════════════════════════════════════════════════
# QUE FAIT run_pending() EXACTEMENT ?
# ═══════════════════════════════════════════════════════════════

# schedule.run_pending() fait 2 choses :
# 1. Vérifie TOUTES les tâches planifiées
# 2. Exécute celles dont l'heure d'exécution est arrivée

# VISUALISATION DU FONCTIONNEMENT :

import schedule
import time

def tache1():
    print("Tâche 1 exécutée")

def tache2():
    print("Tâche 2 exécutée")

# Planification
schedule.every(5).seconds.do(tache1)
schedule.every(10).seconds.do(tache2)

# TIMELINE (ce qui se passe) :
# Seconde 0  : Démarrage
# Seconde 1  : run_pending() vérifie -> Rien à faire
# Seconde 2  : run_pending() vérifie -> Rien à faire
# Seconde 3  : run_pending() vérifie -> Rien à faire
# Seconde 4  : run_pending() vérifie -> Rien à faire
# Seconde 5  : run_pending() vérifie -> Exécute tache1
# Seconde 6  : run_pending() vérifie -> Rien à faire
# Seconde 10 : run_pending() vérifie -> Exécute tache1 ET tache2
# Seconde 15 : run_pending() vérifie -> Exécute tache1
# ...

# ═══════════════════════════════════════════════════════════════
# POURQUOI time.sleep(1) ?
# ═══════════════════════════════════════════════════════════════

# SANS sleep -> CPU à 100%
while True:
    schedule.run_pending()  # Vérifie en boucle sans pause = CPU surchargé!

# AVEC sleep(1) -> CPU normal
while True:
    schedule.run_pending()  # Vérifie
    time.sleep(1)           # Pause 1 seconde -> CPU repose

# ANALOGIE : Imaginez que vous vérifiez votre boîte aux lettres
# Sans sleep : Vous regardez 1 million de fois par seconde (épuisant!)
# Avec sleep(1) : Vous regardez 1 fois par seconde (raisonnable)

# QUELLE DURÉE DE SLEEP ?
time.sleep(1)    # [OK] Bon pour la plupart des cas (précision de 1 seconde)
time.sleep(0.1)  # [OK] Si vous avez besoin de plus de précision
time.sleep(10)   # [X] Trop long : vos tâches auront du retard
time.sleep(60)   # [X] Beaucoup trop long !

# ═══════════════════════════════════════════════════════════════
# ARRÊTER LE LOOP PROPREMENT
# ═══════════════════════════════════════════════════════════════

# PROBLÈME : Ctrl+C arrête brutalement
while True:
    schedule.run_pending()
    time.sleep(1)
# Appuyer Ctrl+C -> Erreur KeyboardInterrupt

# SOLUTION : Gérer Ctrl+C gracieusement
try:
    while True:
        schedule.run_pending()
        time.sleep(1)
except KeyboardInterrupt:
    print("\n[OK] Programme arrêté proprement")
    # Nettoyage si nécessaire
    # fermer_connexions()
    # sauvegarder_etat()


[OK] SYNTAXE COMPLÈTE - TOUTES LES OPTIONS


[OK] PLANIFICATION PAR INTERVALLE

# === Secondes ===
schedule.every(10).seconds.do(job)
schedule.every(30).seconds.do(job)
schedule.every().second.do(job)              # Chaque seconde

# === Minutes ===
schedule.every(5).minutes.do(job)
schedule.every(15).minutes.do(job)
schedule.every().minute.do(job)              # Chaque minute

# === Heures ===
schedule.every(2).hours.do(job)
schedule.every().hour.do(job)                # Chaque heure

# === Jours ===
schedule.every(1).day.do(job)
schedule.every(3).days.do(job)
schedule.every().day.do(job)                 # Chaque jour

# === Semaines ===
schedule.every(1).week.do(job)
schedule.every(2).weeks.do(job)
schedule.every().week.do(job)                # Chaque semaine


[OK] PLANIFICATION À HEURE PRÉCISE

# === Heure spécifique (format 24h) ===
schedule.every().day.at("10:30").do(job)
schedule.every().day.at("14:00").do(job)
schedule.every().day.at("23:59").do(job)

# === Avec secondes ===
schedule.every().day.at("10:30:42").do(job)

# === Plusieurs fois par jour ===
schedule.every().day.at("09:00").do(job)
schedule.every().day.at("12:00").do(job)
schedule.every().day.at("18:00").do(job)

# === Heures avec intervalles ===
schedule.every().hour.at(":17").do(job)      # Chaque heure à 17 minutes
schedule.every().hour.at(":00").do(job)      # Chaque heure pile
schedule.every().hour.at(":30").do(job)      # Chaque heure à 30 minutes

# === Minutes avec secondes ===
schedule.every().minute.at(":23").do(job)    # Chaque minute à 23 secondes


[OK] PLANIFICATION PAR JOUR DE LA SEMAINE

# === Jours spécifiques ===
schedule.every().monday.do(job)
schedule.every().tuesday.do(job)
schedule.every().wednesday.do(job)
schedule.every().thursday.do(job)
schedule.every().friday.do(job)
schedule.every().saturday.do(job)
schedule.every().sunday.do(job)

# === Avec heure précise ===
schedule.every().monday.at("10:30").do(job)
schedule.every().friday.at("17:00").do(job)

# === Plusieurs jours ===
schedule.every().monday.at("09:00").do(job)
schedule.every().wednesday.at("09:00").do(job)
schedule.every().friday.at("09:00").do(job)


[OK] FONCTIONS AVEC PARAMÈTRES

# === Passer des arguments ===
def greet(name):
    print(f"Bonjour {name}!")

schedule.every(10).seconds.do(greet, name="Alice")
schedule.every(10).seconds.do(greet, "Bob")

# === Arguments multiples ===
def task(arg1, arg2, kwarg1=None):
    print(f"Args: {arg1}, {arg2}, Kwarg: {kwarg1}")

schedule.every(5).seconds.do(task, "val1", "val2", kwarg1="val3")

# === Lambda functions ===
schedule.every(10).seconds.do(lambda: print("Lambda job"))
schedule.every(5).seconds.do(lambda: greet("Charlie"))


[OK] GESTION DES TÂCHES

# === Obtenir toutes les tâches ===
jobs = schedule.get_jobs()
print(f"Nombre de tâches: {len(jobs)}")

# === Obtenir jobs avec tag ===
jobs = schedule.get_jobs('daily-tasks')

# === Annuler une tâche spécifique ===
job = schedule.every().day.do(task)
schedule.cancel_job(job)

# === Annuler toutes les tâches ===
schedule.clear()

# === Annuler jobs avec tag ===
schedule.clear('daily-tasks')

# === Vérifier tâches en attente ===
import schedule

def job():
    print("Running")

schedule.every(10).seconds.do(job)

# Vérifier
print(f"Idle seconds: {schedule.idle_seconds()}")  # Temps avant prochaine tâche
print(f"Next run: {schedule.next_run()}")          # Prochaine exécution


[OK] TAGS POUR ORGANISER LES TÂCHES

# === Ajouter des tags ===
schedule.every().day.at("10:00").do(job).tag('daily-tasks', 'important')
schedule.every().hour.do(job).tag('hourly', 'monitoring')
schedule.every().monday.do(job).tag('weekly', 'reports')

# === Récupérer jobs par tag ===
daily_jobs = schedule.get_jobs('daily-tasks')
monitoring_jobs = schedule.get_jobs('monitoring')

# === Annuler par tag ===
schedule.clear('daily-tasks')
schedule.clear('monitoring')

# === Exemple d'organisation ===
# Jobs de sauvegarde
schedule.every().day.at("02:00").do(backup_db).tag('backup', 'critical')
schedule.every().sunday.at("03:00").do(full_backup).tag('backup', 'weekly')

# Jobs de monitoring
schedule.every(5).minutes.do(check_servers).tag('monitoring', 'health')
schedule.every(10).minutes.do(check_disk).tag('monitoring', 'resources')

# Jobs de rapports
schedule.every().day.at("09:00").do(daily_report).tag('reports', 'daily')
schedule.every().friday.at("17:00").do(weekly_report).tag('reports', 'weekly')

# Annuler tous les backups
schedule.clear('backup')


[OK] EXÉCUTION UNIQUE (RUN ONCE)

# === Exécuter une seule fois puis annuler ===
def job_once():
    print("Exécution unique")
    return schedule.CancelJob

schedule.every(5).seconds.do(job_once)

# === Avec condition ===
counter = 0

def job_limited():
    global counter
    counter += 1
    print(f"Exécution {counter}")
    if counter >= 5:
        return schedule.CancelJob

schedule.every(2).seconds.do(job_limited)

# === Exécuter jusqu'à une heure précise ===
from datetime import datetime

def job_until_time():
    print("Running...")
    if datetime.now().hour >= 18:  # Arrêter à 18h
        return schedule.CancelJob

schedule.every(1).hour.do(job_until_time)


[OK] EXÉCUTION CONDITIONNELLE

# === Exécuter seulement si condition vraie ===
def should_run():
    # Vérifier une condition
    return datetime.now().weekday() < 5  # Seulement en semaine

def conditional_job():
    if should_run():
        print("Job exécuté")
    else:
        print("Job ignoré")

schedule.every().day.at("10:00").do(conditional_job)

# === Avec décorateur ===
def weekday_only(job_func):
    def wrapper():
        if datetime.now().weekday() < 5:
            return job_func()
    return wrapper

@weekday_only
def business_job():
    print("Tâche professionnelle")

schedule.every().day.at("09:00").do(business_job)


[OK] GESTION DU TEMPS D'INACTIVITÉ

# === Temps avant prochaine exécution ===
schedule.every(10).seconds.do(job)

# Obtenir le temps d'attente
idle_time = schedule.idle_seconds()
print(f"Prochaine tâche dans {idle_time} secondes")

# === Prochaine exécution ===
next_run = schedule.next_run()
print(f"Prochaine exécution: {next_run}")

# === Ajuster le sleep dynamiquement ===
while True:
    schedule.run_pending()
    # Dormir jusqu'à la prochaine tâche (max 60 secondes)
    time.sleep(min(schedule.idle_seconds() or 60, 60))


[OK] GESTION DES ERREURS

# === Try-except dans la fonction ===
def safe_job():
    try:
        # Code qui peut échouer
        risky_operation()
    except Exception as e:
        print(f"Erreur: {e}")
        # Logger l'erreur
        logging.error(f"Job failed: {e}")

schedule.every(5).minutes.do(safe_job)

# === Wrapper pour toutes les tâches ===
def error_handler(job_func):
    def wrapper(*args, **kwargs):
        try:
            return job_func(*args, **kwargs)
        except Exception as e:
            print(f"Erreur dans {job_func.__name__}: {e}")
            # Ne pas annuler la tâche, continuer
    return wrapper

@error_handler
def risky_job():
    # Code potentiellement problématique
    pass

schedule.every(10).seconds.do(risky_job)

# === Try-except dans le loop principal ===
while True:
    try:
        schedule.run_pending()
        time.sleep(1)
    except KeyboardInterrupt:
        print("Arrêt du scheduler")
        break
    except Exception as e:
        print(f"Erreur dans le loop: {e}")
        # Continuer malgré l'erreur
        time.sleep(1)


[OK] LOGGING ET MONITORING

# === Avec logging standard ===
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def logged_job():
    logging.info("Job démarré")
    # Travail
    logging.info("Job terminé")

schedule.every(10).seconds.do(logged_job)

# === Decorator pour logger automatiquement ===
import functools
import time

def log_execution(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        logging.info(f"Début de {func.__name__}")
        
        try:
            result = func(*args, **kwargs)
            duration = time.time() - start
            logging.info(f"{func.__name__} terminé en {duration:.2f}s")
            return result
        except Exception as e:
            logging.error(f"Erreur dans {func.__name__}: {e}")
            raise
    
    return wrapper

@log_execution
def important_job():
    print("Travail important")

schedule.every(5).minutes.do(important_job)

# === Monitoring des tâches ===
def monitor_jobs():
    jobs = schedule.get_jobs()
    print(f"\n=== Statut des tâches ({len(jobs)} actives) ===")
    for job in jobs:
        print(f"Job: {job.job_func.__name__}")
        print(f"  Prochaine exécution: {job.next_run}")
        print(f"  Intervalle: {job.interval} {job.unit}")
        print(f"  Tags: {job.tags}")
        print()

# Exécuter monitoring toutes les heures
schedule.every().hour.do(monitor_jobs)


[OK] PATTERNS AVANCÉS

# === Job avec état ===
class StatefulJob:
    def __init__(self):
        self.counter = 0
    
    def run(self):
        self.counter += 1
        print(f"Exécution #{self.counter}")

job_instance = StatefulJob()
schedule.every(5).seconds.do(job_instance.run)

# === Job avec retry ===
def job_with_retry(max_retries=3):
    retries = 0
    
    def inner_job():
        nonlocal retries
        try:
            # Opération risquée
            result = risky_operation()
            retries = 0  # Reset sur succès
            return result
        except Exception as e:
            retries += 1
            if retries >= max_retries:
                logging.error(f"Échec après {retries} tentatives: {e}")
                retries = 0  # Reset
            else:
                logging.warning(f"Tentative {retries}/{max_retries} échouée")
    
    return inner_job

schedule.every(10).minutes.do(job_with_retry(max_retries=5))

# === Job avec timeout ===
import signal

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("Job timeout")

def job_with_timeout(timeout_seconds=30):
    def wrapper(func):
        def inner(*args, **kwargs):
            # Set alarm
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(timeout_seconds)
            
            try:
                result = func(*args, **kwargs)
                signal.alarm(0)  # Cancel alarm
                return result
            except TimeoutError:
                logging.error(f"{func.__name__} a dépassé {timeout_seconds}s")
            finally:
                signal.alarm(0)
        
        return inner
    return wrapper

@job_with_timeout(30)
def slow_job():
    # Travail qui peut être long
    pass

schedule.every(5).minutes.do(slow_job)


[OK] PARALLÉLISME & THREADING

# === Threading pour jobs longs ===
import threading

def long_running_job():
    print("Job long démarré")
    time.sleep(30)
    print("Job long terminé")

def threaded_job(func):
    job_thread = threading.Thread(target=func)
    job_thread.start()

schedule.every(10).seconds.do(threaded_job, long_running_job)

# === Thread pool pour limiter concurrence ===
from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=3)

def submit_job(func, *args, **kwargs):
    executor.submit(func, *args, **kwargs)

def worker_job():
    print(f"Worker dans thread {threading.current_thread().name}")
    time.sleep(5)

schedule.every(2).seconds.do(submit_job, worker_job)

# === Background scheduler ===
class BackgroundScheduler:
    def __init__(self):
        self.stop_flag = threading.Event()
        self.thread = None
    
    def run(self):
        while not self.stop_flag.is_set():
            schedule.run_pending()
            time.sleep(1)
    
    def start(self):
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.thread.start()
    
    def stop(self):
        self.stop_flag.set()
        if self.thread:
            self.thread.join()

# Utilisation
scheduler = BackgroundScheduler()
schedule.every(10).seconds.do(job)
scheduler.start()

# Votre application continue...
print("Scheduler en arrière-plan")
time.sleep(60)

scheduler.stop()


[OK] INTÉGRATION AVEC BASES DE DONNÉES

# === SQLite pour persistance ===
import sqlite3
from datetime import datetime

def init_db():
    conn = sqlite3.connect('scheduler.db')
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS job_history
                 (id INTEGER PRIMARY KEY, job_name TEXT, 
                  execution_time TEXT, status TEXT, message TEXT)''')
    conn.commit()
    conn.close()

def log_job_execution(job_name, status, message=""):
    conn = sqlite3.connect('scheduler.db')
    c = conn.cursor()
    c.execute("INSERT INTO job_history VALUES (NULL, ?, ?, ?, ?)",
              (job_name, datetime.now().isoformat(), status, message))
    conn.commit()
    conn.close()

def tracked_job():
    job_name = "data_sync"
    try:
        # Votre logique
        print("Synchronisation données...")
        log_job_execution(job_name, "SUCCESS")
    except Exception as e:
        log_job_execution(job_name, "FAILED", str(e))
        raise

init_db()
schedule.every(10).minutes.do(tracked_job)

# === Charger jobs depuis base de données ===
def load_jobs_from_db():
    conn = sqlite3.connect('config.db')
    c = conn.cursor()
    c.execute("SELECT job_name, schedule_type, time FROM scheduled_jobs")
    
    for row in c.fetchall():
        job_name, schedule_type, time_str = row
        
        # Créer job dynamiquement
        if schedule_type == 'daily':
            schedule.every().day.at(time_str).do(globals()[job_name])
        elif schedule_type == 'hourly':
            schedule.every().hour.do(globals()[job_name])
    
    conn.close()

load_jobs_from_db()


[OK] EXEMPLES PRATIQUES COMPLETS

# === 1. Système de backup ===
import shutil
from pathlib import Path

class BackupSystem:
    def __init__(self, source_dir, backup_dir):
        self.source = Path(source_dir)
        self.backup_root = Path(backup_dir)
        self.backup_root.mkdir(exist_ok=True)
    
    def incremental_backup(self):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_dir = self.backup_root / f"backup_{timestamp}"
        
        try:
            shutil.copytree(self.source, backup_dir)
            logging.info(f"Backup créé: {backup_dir}")
        except Exception as e:
            logging.error(f"Backup échoué: {e}")
    
    def full_backup(self):
        # Backup complet avec compression
        timestamp = datetime.now().strftime("%Y%m%d")
        archive_name = self.backup_root / f"full_backup_{timestamp}"
        
        try:
            shutil.make_archive(archive_name, 'gztar', self.source)
            logging.info(f"Backup complet: {archive_name}.tar.gz")
        except Exception as e:
            logging.error(f"Backup complet échoué: {e}")
    
    def cleanup_old_backups(self, days=7):
        # Supprimer backups de plus de X jours
        cutoff = datetime.now() - timedelta(days=days)
        
        for backup in self.backup_root.iterdir():
            if backup.stat().st_mtime < cutoff.timestamp():
                if backup.is_dir():
                    shutil.rmtree(backup)
                else:
                    backup.unlink()
                logging.info(f"Backup supprimé: {backup}")

# Configuration
backup_system = BackupSystem("/data/important", "/backups")

# Planification
schedule.every().day.at("02:00").do(backup_system.incremental_backup).tag('backup')
schedule.every().sunday.at("03:00").do(backup_system.full_backup).tag('backup')
schedule.every().day.at("04:00").do(backup_system.cleanup_old_backups).tag('maintenance')

# === 2. Monitoring de serveur ===
import requests
import psutil

class ServerMonitor:
    def __init__(self, alert_threshold=80):
        self.threshold = alert_threshold
    
    def check_cpu(self):
        cpu_percent = psutil.cpu_percent(interval=1)
        if cpu_percent > self.threshold:
            self.send_alert(f"CPU élevé: {cpu_percent}%")
        logging.info(f"CPU: {cpu_percent}%")
    
    def check_memory(self):
        memory = psutil.virtual_memory()
        if memory.percent > self.threshold:
            self.send_alert(f"Mémoire élevée: {memory.percent}%")
        logging.info(f"Mémoire: {memory.percent}%")
    
    def check_disk(self):
        disk = psutil.disk_usage('/')
        if disk.percent > self.threshold:
            self.send_alert(f"Disque plein: {disk.percent}%")
        logging.info(f"Disque: {disk.percent}%")
    
    def check_website(self, url):
        try:
            response = requests.get(url, timeout=5)
            if response.status_code != 200:
                self.send_alert(f"{url} retourne {response.status_code}")
        except Exception as e:
            self.send_alert(f"{url} inaccessible: {e}")
    
    def send_alert(self, message):
        # Envoyer email, Slack, etc.
        logging.warning(f"ALERTE: {message}")

monitor = ServerMonitor(alert_threshold=85)

schedule.every(5).minutes.do(monitor.check_cpu).tag('monitoring')
schedule.every(5).minutes.do(monitor.check_memory).tag('monitoring')
schedule.every(15).minutes.do(monitor.check_disk).tag('monitoring')
schedule.every(2).minutes.do(monitor.check_website, "https://myapp.com").tag('monitoring')

# === 3. Scraper web périodique ===
import requests
from bs4 import BeautifulSoup

class WebScraper:
    def __init__(self, url, db_path):
        self.url = url
        self.db_path = db_path
    
    def scrape_prices(self):
        try:
            response = requests.get(self.url)
            soup = BeautifulSoup(response.content, 'html.parser')
            
            prices = []
            for item in soup.select('.product-price'):
                price = float(item.text.strip('$'))
                prices.append(price)
            
            self.save_to_db(prices)
            logging.info(f"Scraped {len(prices)} prices")
            
        except Exception as e:
            logging.error(f"Scraping failed: {e}")
    
    def save_to_db(self, prices):
        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        timestamp = datetime.now()
        
        for price in prices:
            c.execute("INSERT INTO prices VALUES (?, ?)", (timestamp, price))
        
        conn.commit()
        conn.close()
    
    def analyze_trends(self):
        # Analyser les tendances de prix
        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        
        # Prix moyens des 7 derniers jours
        c.execute("""SELECT AVG(price) FROM prices 
                     WHERE timestamp > datetime('now', '-7 days')""")
        avg_price = c.fetchone()[0]
        
        logging.info(f"Prix moyen 7j: ${avg_price:.2f}")
        conn.close()

scraper = WebScraper("https://example.com/products", "prices.db")

schedule.every(6).hours.do(scraper.scrape_prices).tag('scraper')
schedule.every().day.at("09:00").do(scraper.analyze_trends).tag('analysis')

# === 4. Nettoyage automatique ===
class FileCleanup:
    def __init__(self, directories):
        self.directories = directories
    
    def cleanup_temp_files(self):
        for directory in self.directories:
            path = Path(directory)
            
            # Supprimer fichiers temporaires
            for temp_file in path.glob('*.tmp'):
                temp_file.unlink()
                logging.info(f"Supprimé: {temp_file}")
            
            # Supprimer vieux logs
            for log_file in path.glob('*.log'):
                if self._is_old(log_file, days=30):
                    log_file.unlink()
                    logging.info(f"Supprimé: {log_file}")
    
    def _is_old(self, file_path, days):
        file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
        return (datetime.now() - file_time).days > days
    
    def compress_old_logs(self):
        for directory in self.directories:
            path = Path(directory)
            
            for log_file in path.glob('*.log'):
                if self._is_old(log_file, days=7):
                    # Compresser
                    shutil.make_archive(
                        str(log_file), 'gztar', str(log_file.parent), log_file.name
                    )
                    log_file.unlink()

cleanup = FileCleanup(['/tmp', '/var/log/myapp'])

schedule.every().day.at("03:00").do(cleanup.cleanup_temp_files).tag('maintenance')
schedule.every().sunday.at("04:00").do(cleanup.compress_old_logs).tag('maintenance')


[OK] LOOP PRINCIPAL ROBUSTE

# === Version simple ===
while True:
    schedule.run_pending()
    time.sleep(1)

# === Version avec gestion erreurs ===
def run_scheduler():
    while True:
        try:
            schedule.run_pending()
            time.sleep(1)
        except KeyboardInterrupt:
            logging.info("Scheduler arrêté par l'utilisateur")
            break
        except Exception as e:
            logging.error(f"Erreur dans scheduler: {e}")
            time.sleep(5)  # Attendre avant de retry

# === Version optimisée (sleep intelligent) ===
def run_scheduler_optimized():
    while True:
        try:
            schedule.run_pending()
            
            # Calculer temps d'attente optimal
            idle_seconds = schedule.idle_seconds()
            if idle_seconds is None:
                # Aucune tâche planifiée
                time.sleep(60)
            elif idle_seconds > 0:
                # Attendre jusqu'à la prochaine tâche (max 60s)
                time.sleep(min(idle_seconds, 60))
            else:
                # Tâche immédiate
                time.sleep(0.1)
                
        except KeyboardInterrupt:
            logging.info("Arrêt du scheduler")
            break
        except Exception as e:
            logging.error(f"Erreur: {e}")
            time.sleep(5)

# === Version avec signal handling (Unix) ===
import signal
import sys

class GracefulScheduler:
    def __init__(self):
        self.is_running = True
        signal.signal(signal.SIGINT, self.signal_handler)
        signal.signal(signal.SIGTERM, self.signal_handler)
    
    def signal_handler(self, signum, frame):
        logging.info(f"Signal {signum} reçu, arrêt gracieux...")
        self.is_running = False
    
    def run(self):
        while self.is_running:
            schedule.run_pending()
            time.sleep(1)
        
        logging.info("Scheduler arrêté proprement")
        sys.exit(0)

scheduler = GracefulScheduler()
scheduler.run()

# === Version avec health check endpoint ===
from flask import Flask
import threading

app = Flask(__name__)

@app.route('/health')
def health():
    jobs_count = len(schedule.get_jobs())
    next_run = schedule.next_run()
    
    return {
        'status': 'running',
        'jobs': jobs_count,
        'next_run': next_run.isoformat() if next_run else None
    }

@app.route('/jobs')
def jobs_list():
    jobs = []
    for job in schedule.get_jobs():
        jobs.append({
            'function': job.job_func.__name__,
            'interval': job.interval,
            'unit': job.unit,
            'next_run': job.next_run.isoformat(),
            'tags': list(job.tags)
        })
    return {'jobs': jobs}

def run_scheduler():
    while True:
        schedule.run_pending()
        time.sleep(1)

# Démarrer scheduler en background
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
scheduler_thread.start()

# Démarrer Flask
app.run(port=5000)


[OK] ALTERNATIVES À SCHEDULE

# schedule: Simple, léger, pour scripts
# APScheduler: Plus puissant, jobs persistants, multiple backends
# Celery Beat: Pour tâches distribuées, production
# Cron: Système niveau OS
# Airflow: Workflows complexes, data pipelines


[OK] PROJET RÉEL COMPLET : SYSTÈME DE MONITORING POUR DÉVELOPPEUR

# ═══════════════════════════════════════════════════════════════
# CONTEXTE : Vous développez une application web et vous voulez :
# 1. Vérifier que votre site est accessible
# 2. Surveiller l'espace disque
# 3. Sauvegarder la base de données
# 4. Nettoyer les logs anciens
# 5. Générer un rapport quotidien
# 6. Recevoir des alertes par email si problème
# ═══════════════════════════════════════════════════════════════

import schedule
import time
import requests
import shutil
import smtplib
from datetime import datetime
from pathlib import Path
from email.mime.text import MIMEText
import logging

# ═══════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════

# Configuration logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('monitoring.log'),
        logging.StreamHandler()  # Affiche aussi dans console
    ]
)

# Configuration email (pour alertes)
EMAIL_CONFIG = {
    'smtp_server': 'smtp.gmail.com',
    'smtp_port': 587,
    'email_from': 'votre_email@gmail.com',
    'email_password': 'votre_mot_de_passe',  # Utilisez mot de passe application
    'email_to': 'admin@example.com'
}

# Configuration monitoring
WEBSITE_URL = "https://votre-site.com"
DISK_WARNING_THRESHOLD = 80  # % d'utilisation disque
LOG_RETENTION_DAYS = 7
BACKUP_DIR = Path("backups")
LOG_DIR = Path("logs")

# ═══════════════════════════════════════════════════════════════
# FONCTION UTILITAIRE : ENVOYER EMAIL
# ═══════════════════════════════════════════════════════════════

def envoyer_alerte_email(sujet, message):
    """
    Envoie un email d'alerte à l'administrateur
    
    Pourquoi cette fonction ?
    - Centralise la logique d'envoi d'email
    - Peut être réutilisée par toutes les tâches
    - Gère les erreurs proprement
    """
    try:
        msg = MIMEText(message)
        msg['Subject'] = f"[ALERTE] ALERTE MONITORING: {sujet}"
        msg['From'] = EMAIL_CONFIG['email_from']
        msg['To'] = EMAIL_CONFIG['email_to']
        
        with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
            server.starttls()
            server.login(EMAIL_CONFIG['email_from'], EMAIL_CONFIG['email_password'])
            server.send_message(msg)
        
        logging.info(f"Email d'alerte envoyé : {sujet}")
    except Exception as e:
        logging.error(f"Erreur envoi email : {e}")

# ═══════════════════════════════════════════════════════════════
# TÂCHE 1 : VÉRIFIER ACCESSIBILITÉ DU SITE
# ═══════════════════════════════════════════════════════════════

def verifier_site_web():
    """
    Vérifie si le site est accessible et répond correctement
    
    S'exécute : Toutes les 5 minutes
    Alerte si : Site inaccessible ou erreur HTTP
    """
    logging.info(f"Vérification de {WEBSITE_URL}...")
    
    try:
        # Tentative de connexion avec timeout de 10 secondes
        response = requests.get(WEBSITE_URL, timeout=10)
        
        if response.status_code == 200:
            logging.info(f"[OK] Site accessible (temps de réponse: {response.elapsed.total_seconds():.2f}s)")
        else:
            # Site accessible mais erreur HTTP
            message = f"Le site retourne le code HTTP {response.status_code}"
            logging.warning(message)
            envoyer_alerte_email("Erreur HTTP", message)
    
    except requests.exceptions.Timeout:
        message = f"{WEBSITE_URL} ne répond pas (timeout après 10s)"
        logging.error(message)
        envoyer_alerte_email("Site Timeout", message)
    
    except requests.exceptions.ConnectionError:
        message = f"{WEBSITE_URL} est inaccessible (erreur de connexion)"
        logging.error(message)
        envoyer_alerte_email("Site Inaccessible", message)
    
    except Exception as e:
        message = f"Erreur lors de la vérification : {e}"
        logging.error(message)
        envoyer_alerte_email("Erreur Monitoring", message)

# ═══════════════════════════════════════════════════════════════
# TÂCHE 2 : SURVEILLER L'ESPACE DISQUE
# ═══════════════════════════════════════════════════════════════

def verifier_espace_disque():
    """
    Vérifie l'espace disque disponible
    
    S'exécute : Toutes les 30 minutes
    Alerte si : Utilisation > 80%
    """
    logging.info("Vérification espace disque...")
    
    try:
        # Obtenir statistiques du disque
        usage = shutil.disk_usage("/")
        
        # Calculer le pourcentage utilisé
        percent_used = (usage.used / usage.total) * 100
        
        # Convertir en GB pour affichage
        gb_used = usage.used / (1024**3)
        gb_total = usage.total / (1024**3)
        gb_free = usage.free / (1024**3)
        
        logging.info(f"Disque: {gb_used:.1f}GB / {gb_total:.1f}GB utilisé ({percent_used:.1f}%)")
        
        # Alerte si seuil dépassé
        if percent_used > DISK_WARNING_THRESHOLD:
            message = f"""
            [ATTENTION] ATTENTION : Espace disque faible !
            
            Utilisé : {gb_used:.1f} GB ({percent_used:.1f}%)
            Total : {gb_total:.1f} GB
            Libre : {gb_free:.1f} GB
            
            Seuil d'alerte : {DISK_WARNING_THRESHOLD}%
            """
            logging.warning(f"Espace disque critique : {percent_used:.1f}%")
            envoyer_alerte_email("Espace Disque Critique", message)
    
    except Exception as e:
        logging.error(f"Erreur vérification disque : {e}")

# ═══════════════════════════════════════════════════════════════
# TÂCHE 3 : SAUVEGARDER LA BASE DE DONNÉES
# ═══════════════════════════════════════════════════════════════

def sauvegarder_base_donnees():
    """
    Crée une sauvegarde de la base de données
    
    S'exécute : Tous les jours à 2h du matin
    Sauvegarde : Fichier horodaté dans le dossier backups/
    """
    logging.info("Démarrage sauvegarde base de données...")
    
    try:
        # Créer le dossier de backup s'il n'existe pas
        BACKUP_DIR.mkdir(exist_ok=True)
        
        # Générer nom de fichier avec timestamp
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_file = BACKUP_DIR / f"db_backup_{timestamp}.sql"
        
        # OPTION 1 : Pour PostgreSQL
        # import subprocess
        # subprocess.run([
        #     'pg_dump',
        #     '-U', 'username',
        #     '-d', 'database_name',
        #     '-f', str(backup_file)
        # ], check=True)
        
        # OPTION 2 : Pour MySQL
        # subprocess.run([
        #     'mysqldump',
        #     '-u', 'username',
        #     '-p', 'password',
        #     'database_name',
        #     '>', str(backup_file)
        # ], check=True, shell=True)
        
        # SIMULATION (pour cet exemple)
        with open(backup_file, 'w') as f:
            f.write(f"Backup simulé créé à {datetime.now()}\n")
            f.write("-- Ici irait le dump de votre base de données\n")
        
        logging.info(f"[OK] Backup créé : {backup_file}")
        
        # Nettoyer les vieux backups (garder seulement 7 derniers jours)
        nettoyer_vieux_backups()
    
    except Exception as e:
        message = f"Erreur lors de la sauvegarde : {e}"
        logging.error(message)
        envoyer_alerte_email("Échec Sauvegarde", message)

def nettoyer_vieux_backups():
    """Supprime les backups de plus de 7 jours"""
    from datetime import timedelta
    
    cutoff_date = datetime.now() - timedelta(days=LOG_RETENTION_DAYS)
    
    for backup_file in BACKUP_DIR.glob("db_backup_*.sql"):
        # Vérifier la date de modification du fichier
        file_time = datetime.fromtimestamp(backup_file.stat().st_mtime)
        
        if file_time < cutoff_date:
            backup_file.unlink()
            logging.info(f"Backup ancien supprimé : {backup_file.name}")

# ═══════════════════════════════════════════════════════════════
# TÂCHE 4 : NETTOYER LES LOGS ANCIENS
# ═══════════════════════════════════════════════════════════════

def nettoyer_logs_anciens():
    """
    Supprime les fichiers logs de plus de 7 jours
    
    S'exécute : Tous les jours à 3h du matin
    Nettoie : Fichiers .log dans le dossier logs/
    """
    logging.info("Nettoyage des logs anciens...")
    
    try:
        if not LOG_DIR.exists():
            logging.info("Dossier logs n'existe pas, rien à nettoyer")
            return
        
        from datetime import timedelta
        cutoff_date = datetime.now() - timedelta(days=LOG_RETENTION_DAYS)
        
        files_deleted = 0
        space_freed = 0
        
        # Parcourir tous les fichiers .log
        for log_file in LOG_DIR.glob("*.log"):
            file_time = datetime.fromtimestamp(log_file.stat().st_mtime)
            
            if file_time < cutoff_date:
                file_size = log_file.stat().st_size
                log_file.unlink()
                files_deleted += 1
                space_freed += file_size
                logging.info(f"Log supprimé : {log_file.name}")
        
        space_freed_mb = space_freed / (1024**2)
        logging.info(f"[OK] Nettoyage terminé : {files_deleted} fichiers supprimés ({space_freed_mb:.2f} MB libérés)")
    
    except Exception as e:
        logging.error(f"Erreur nettoyage logs : {e}")

# ═══════════════════════════════════════════════════════════════
# TÂCHE 5 : GÉNÉRER RAPPORT QUOTIDIEN
# ═══════════════════════════════════════════════════════════════

def generer_rapport_quotidien():
    """
    Génère et envoie un rapport quotidien par email
    
    S'exécute : Tous les jours à 9h du matin
    Contenu : Résumé des activités et statut système
    """
    logging.info("Génération du rapport quotidien...")
    
    try:
        # Collecter les statistiques
        disk_usage = shutil.disk_usage("/")
        percent_used = (disk_usage.used / disk_usage.total) * 100
        
        # Compter les backups existants
        backup_count = len(list(BACKUP_DIR.glob("db_backup_*.sql"))) if BACKUP_DIR.exists() else 0
        
        # Générer le rapport
        rapport = f"""
        [GRAPHIQUE] RAPPORT QUOTIDIEN - {datetime.now().strftime('%d/%m/%Y')}
        
        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        
        [WEB] SITE WEB
        URL : {WEBSITE_URL}
        Statut : Surveillé toutes les 5 minutes
        
        [SAUVEGARDE] ESPACE DISQUE
        Utilisation : {percent_used:.1f}%
        Libre : {disk_usage.free / (1024**3):.1f} GB
        
        [PACKAGE] SAUVEGARDES
        Nombre de backups : {backup_count}
        Dernière sauvegarde : 02:00 (quotidienne)
        
        [NETTOYAGE] MAINTENANCE
        Nettoyage logs : 03:00 (quotidien)
        Rétention : {LOG_RETENTION_DAYS} jours
        
        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        
        [OK] Tous les systèmes fonctionnent normalement
        
        Ce rapport est généré automatiquement tous les jours à 09:00
        """
        
        # Envoyer le rapport par email
        msg = MIMEText(rapport)
        msg['Subject'] = f"[GRAPHIQUE] Rapport Quotidien - {datetime.now().strftime('%d/%m/%Y')}"
        msg['From'] = EMAIL_CONFIG['email_from']
        msg['To'] = EMAIL_CONFIG['email_to']
        
        with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
            server.starttls()
            server.login(EMAIL_CONFIG['email_from'], EMAIL_CONFIG['email_password'])
            server.send_message(msg)
        
        logging.info("[OK] Rapport quotidien envoyé par email")
    
    except Exception as e:
        logging.error(f"Erreur génération rapport : {e}")

# ═══════════════════════════════════════════════════════════════
# TÂCHE 6 : AFFICHER STATUT DU SCHEDULER (POUR DEBUG)
# ═══════════════════════════════════════════════════════════════

def afficher_statut():
    """
    Affiche le statut de toutes les tâches planifiées
    
    S'exécute : Toutes les heures
    Utile pour : Vérifier que tout fonctionne correctement
    """
    jobs = schedule.get_jobs()
    
    print("\n" + "="*60)
    print(f"[LISTE] STATUT DU SCHEDULER - {datetime.now().strftime('%H:%M:%S')}")
    print("="*60)
    print(f"Nombre de tâches actives : {len(jobs)}")
    print("\nProchaines exécutions :")
    
    for job in jobs:
        fonction = job.job_func.__name__
        prochaine = job.next_run.strftime('%d/%m/%Y %H:%M:%S') if job.next_run else "N/A"
        print(f"  • {fonction:30s} -> {prochaine}")
    
    print("="*60 + "\n")

# ═══════════════════════════════════════════════════════════════
# CONFIGURATION DES PLANIFICATIONS
# ═══════════════════════════════════════════════════════════════

def configurer_scheduler():
    """
    Configure toutes les tâches planifiées
    
    Cette fonction centralise toute la configuration.
    Facilite la maintenance et la lecture du code.
    """
    logging.info("Configuration du scheduler...")
    
    # Monitoring continu
    schedule.every(5).minutes.do(verifier_site_web)
    schedule.every(30).minutes.do(verifier_espace_disque)
    
    # Tâches quotidiennes (heures creuses la nuit)
    schedule.every().day.at("02:00").do(sauvegarder_base_donnees)
    schedule.every().day.at("03:00").do(nettoyer_logs_anciens)
    
    # Rapport matinal
    schedule.every().day.at("09:00").do(generer_rapport_quotidien)
    
    # Statut régulier (pour debugging)
    schedule.every().hour.do(afficher_statut)
    
    logging.info(f"[OK] {len(schedule.get_jobs())} tâches configurées")
    
    # Afficher le statut initial
    afficher_statut()

# ═══════════════════════════════════════════════════════════════
# BOUCLE PRINCIPALE
# ═══════════════════════════════════════════════════════════════

def demarrer_monitoring():
    """
    Démarre le système de monitoring
    
    Cette fonction :
    1. Configure toutes les tâches
    2. Lance la boucle principale
    3. Gère l'arrêt gracieux (Ctrl+C)
    """
    print("\n" + "="*60)
    print("[RAPIDE] DÉMARRAGE DU SYSTÈME DE MONITORING")
    print("="*60)
    
    # Configurer le scheduler
    configurer_scheduler()
    
    print("\n[IDEE] Le monitoring est actif. Appuyez Ctrl+C pour arrêter.\n")
    
    try:
        # Boucle principale infinie
        while True:
            schedule.run_pending()  # Exécuter les tâches en attente
            time.sleep(1)           # Pause de 1 seconde
    
    except KeyboardInterrupt:
        # L'utilisateur a appuyé Ctrl+C
        print("\n\n" + "="*60)
        print("[STOP] ARRÊT DU MONITORING")
        print("="*60)
        logging.info("Arrêt du scheduler demandé par l'utilisateur")
        
        # Nettoyage si nécessaire
        # fermer_connexions_db()
        # sauvegarder_etat()
        
        print("[OK] Monitoring arrêté proprement\n")

# ═══════════════════════════════════════════════════════════════
# POINT D'ENTRÉE DU PROGRAMME
# ═══════════════════════════════════════════════════════════════

if __name__ == '__main__':
    # Créer les dossiers nécessaires
    BACKUP_DIR.mkdir(exist_ok=True)
    LOG_DIR.mkdir(exist_ok=True)
    
    # Démarrer le monitoring
    demarrer_monitoring()

# ═══════════════════════════════════════════════════════════════
# COMMENT UTILISER CE PROGRAMME ?
# ═══════════════════════════════════════════════════════════════

"""
1. CONFIGURATION INITIALE :
   - Modifiez les variables en haut du fichier (EMAIL_CONFIG, WEBSITE_URL, etc.)
   - Créez un mot de passe d'application Gmail si vous utilisez Gmail
   
2. INSTALLATION DÉPENDANCES :
   pip install schedule requests
   
3. EXÉCUTION :
   python monitoring_system.py
   
4. EN PRODUCTION :
   - Utilisez systemd (Linux) pour démarrer automatiquement
   - Ou Docker pour containeriser
   - Ou screen/tmux pour garder en arrière-plan
   
5. PERSONNALISATION :
   - Ajoutez vos propres tâches en créant de nouvelles fonctions
   - Modifiez les horaires dans configurer_scheduler()
   - Adaptez les seuils d'alerte à vos besoins

[DOCS] CE QUE VOUS APPRENEZ AVEC CET EXEMPLE :
[OK] Structurer un projet avec schedule
[OK] Gérer plusieurs tâches avec des fréquences différentes
[OK] Envoyer des alertes par email
[OK] Logger proprement les événements
[OK] Gérer les erreurs gracieusement
[OK] Créer un système de monitoring complet
[OK] Organiser le code de manière professionnelle
"""

# === schedule (Python) ===
# [OK] Syntaxe Python claire
# [OK] Facile à debugger
# [OK] Intégration avec code Python
# [OK] Pas besoin de permissions système
# [X] Nécessite process Python actif
# [X] Pas de persistance

schedule.every().day.at("10:30").do(job)

# === cron (Système) ===
# [OK] Niveau système, robuste
# [OK] Fonctionne même si script arrêté
# [OK] Gestion par l'OS
# [X] Syntaxe complexe
# [X] Difficile à debugger
# [X] Moins flexible

# Crontab: 30 10 * * * /usr/bin/python3 /path/to/script.py


[OK] MIGRATION VERS APSCHEDULER

# APScheduler offre plus de fonctionnalités
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger

scheduler = BlockingScheduler()

# Équivalent schedule
# schedule.every().day.at("10:30").do(job)
scheduler.add_job(job, CronTrigger(hour=10, minute=30))

# schedule.every(10).seconds.do(job)
scheduler.add_job(job, 'interval', seconds=10)

scheduler.start()


[OK] CONFIGURATION AVANCÉE

# === Fichier de configuration (YAML) ===
# schedule_config.yaml
import yaml

"""
jobs:
  - name: backup
    function: backup_database
    schedule:
      type: daily
      time: "02:00"
    tags: [backup, critical]
  
  - name: cleanup
    function: cleanup_temp
    schedule:
      type: interval
      seconds: 3600
    tags: [maintenance]
  
  - name: report
    function: generate_report
    schedule:
      type: weekly
      day: monday
      time: "09:00"
    tags: [reports]
"""

def load_schedule_from_config(config_file):
    with open(config_file, 'r') as f:
        config = yaml.safe_load(f)
    
    for job_config in config['jobs']:
        func = globals()[job_config['function']]
        sched = job_config['schedule']
        tags = job_config.get('tags', [])
        
        if sched['type'] == 'daily':
            job = schedule.every().day.at(sched['time']).do(func)
        elif sched['type'] == 'interval':
            seconds = sched['seconds']
            job = schedule.every(seconds).seconds.do(func)
        elif sched['type'] == 'weekly':
            day = sched['day']
            time = sched['time']
            job = getattr(schedule.every(), day).at(time).do(func)
        
        for tag in tags:
            job.tag(tag)

load_schedule_from_config('schedule_config.yaml')

# === Fichier de configuration (JSON) ===
import json

def load_schedule_from_json(json_file):
    with open(json_file, 'r') as f:
        config = json.load(f)
    
    for job in config['jobs']:
        func = globals()[job['function']]
        
        if job['type'] == 'cron':
            schedule.every().day.at(job['time']).do(func)
        elif job['type'] == 'interval':
            interval = job['interval']
            unit = job['unit']
            getattr(schedule.every(interval), unit).do(func)


[OK] PATTERNS DE DÉPLOIEMENT

# === 1. Script standalone ===
#!/usr/bin/env python3
import schedule
import time

def job():
    print("Running job")

schedule.every(10).seconds.do(job)

if __name__ == '__main__':
    while True:
        schedule.run_pending()
        time.sleep(1)

# Exécuter: python scheduler.py

# === 2. Service systemd (Linux) ===
# /etc/systemd/system/myscheduler.service
"""
[Unit]
Description=My Python Scheduler
After=network.target

[Service]
Type=simple
User=myuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/scheduler.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
"""

# Commandes:
# sudo systemctl enable myscheduler
# sudo systemctl start myscheduler
# sudo systemctl status myscheduler

# === 3. Docker container ===
# Dockerfile
"""
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY scheduler.py .
COPY jobs/ ./jobs/

CMD ["python", "scheduler.py"]
"""

# docker-compose.yml
"""
version: '3.8'

services:
  scheduler:
    build: .
    restart: unless-stopped
    environment:
      - TZ=Europe/Paris
    volumes:
      - ./logs:/app/logs
      - ./data:/app/data
"""

# === 4. Supervisord ===
# /etc/supervisor/conf.d/scheduler.conf
"""
[program:scheduler]
command=/usr/bin/python3 /opt/myapp/scheduler.py
directory=/opt/myapp
user=myuser
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/scheduler.log
"""

# === 5. Screen/tmux (développement) ===
# screen -dmS scheduler python scheduler.py
# screen -r scheduler  # Attacher

# tmux new -d -s scheduler "python scheduler.py"
# tmux attach -t scheduler


[OK] DEBUGGING ET TESTING

# === Mode debug ===
import schedule
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

# schedule utilise le logger 'schedule'
schedule_logger = logging.getLogger('schedule')
schedule_logger.setLevel(logging.DEBUG)

def job():
    print("Job running")

schedule.every(5).seconds.do(job)

# === Dry-run (tester sans exécuter) ===
class DryRunScheduler:
    def __init__(self):
        self.jobs = []
    
    def add_job(self, interval, unit, func, *args, **kwargs):
        self.jobs.append({
            'interval': interval,
            'unit': unit,
            'function': func.__name__,
            'args': args,
            'kwargs': kwargs
        })
        print(f"[DRY RUN] Planifié: {func.__name__} tous les {interval} {unit}")
    
    def show_schedule(self):
        print("\n=== Schedule planifié ===")
        for i, job in enumerate(self.jobs, 1):
            print(f"{i}. {job['function']} - {job['interval']} {job['unit']}")

# Utilisation en mode dry-run
dry_scheduler = DryRunScheduler()
dry_scheduler.add_job(10, 'seconds', job)
dry_scheduler.add_job(1, 'hour', another_job)
dry_scheduler.show_schedule()

# === Test unitaire ===
import unittest
from unittest.mock import Mock, patch
from datetime import datetime, timedelta

class TestScheduler(unittest.TestCase):
    def setUp(self):
        schedule.clear()
    
    def tearDown(self):
        schedule.clear()
    
    def test_job_scheduled(self):
        mock_job = Mock()
        schedule.every(10).seconds.do(mock_job)
        
        jobs = schedule.get_jobs()
        self.assertEqual(len(jobs), 1)
    
    def test_job_execution(self):
        mock_job = Mock()
        schedule.every(1).seconds.do(mock_job)
        
        schedule.run_pending()
        time.sleep(2)
        schedule.run_pending()
        
        mock_job.assert_called()
    
    def test_job_with_args(self):
        mock_job = Mock()
        schedule.every(1).seconds.do(mock_job, "arg1", kwarg="value")
        
        time.sleep(2)
        schedule.run_pending()
        
        mock_job.assert_called_with("arg1", kwarg="value")
    
    def test_cancel_job(self):
        mock_job = Mock()
        job = schedule.every(1).seconds.do(mock_job)
        
        self.assertEqual(len(schedule.get_jobs()), 1)
        
        schedule.cancel_job(job)
        
        self.assertEqual(len(schedule.get_jobs()), 0)

if __name__ == '__main__':
    unittest.main()

# === Mock time pour tests rapides ===
def test_schedule_fast():
    from unittest.mock import patch
    
    executed = []
    
    def job():
        executed.append(datetime.now())
    
    schedule.every(10).seconds.do(job)
    
    # Simuler le passage du temps
    with patch('schedule.time') as mock_time:
        mock_time.time.side_effect = [0, 10, 20, 30]  # Temps simulé
        
        for _ in range(3):
            schedule.run_pending()
    
    assert len(executed) == 3


[OK] MONITORING ET MÉTRIQUES

# === Prometheus metrics ===
from prometheus_client import Counter, Gauge, Histogram, start_http_server
import time

# Métriques
job_executions = Counter('scheduler_job_executions_total', 'Total job executions', ['job_name', 'status'])
job_duration = Histogram('scheduler_job_duration_seconds', 'Job duration', ['job_name'])
active_jobs = Gauge('scheduler_active_jobs', 'Number of active jobs')

def monitored_job(func):
    def wrapper(*args, **kwargs):
        job_name = func.__name__
        start_time = time.time()
        
        try:
            result = func(*args, **kwargs)
            job_executions.labels(job_name=job_name, status='success').inc()
            return result
        except Exception as e:
            job_executions.labels(job_name=job_name, status='failed').inc()
            raise
        finally:
            duration = time.time() - start_time
            job_duration.labels(job_name=job_name).observe(duration)
    
    return wrapper

@monitored_job
def my_job():
    print("Working...")
    time.sleep(2)

# Démarrer serveur métriques
start_http_server(8000)

schedule.every(10).seconds.do(my_job)

# Mettre à jour gauge
active_jobs.set(len(schedule.get_jobs()))

# === Dashboard de monitoring ===
class SchedulerDashboard:
    def __init__(self):
        self.execution_history = []
        self.max_history = 100
    
    def record_execution(self, job_name, duration, status):
        self.execution_history.append({
            'timestamp': datetime.now(),
            'job': job_name,
            'duration': duration,
            'status': status
        })
        
        # Limiter historique
        if len(self.execution_history) > self.max_history:
            self.execution_history.pop(0)
    
    def get_stats(self):
        total = len(self.execution_history)
        successful = sum(1 for e in self.execution_history if e['status'] == 'success')
        failed = total - successful
        
        avg_duration = sum(e['duration'] for e in self.execution_history) / total if total > 0 else 0
        
        return {
            'total_executions': total,
            'successful': successful,
            'failed': failed,
            'success_rate': successful / total * 100 if total > 0 else 0,
            'avg_duration': avg_duration
        }
    
    def show_dashboard(self):
        stats = self.get_stats()
        jobs = schedule.get_jobs()
        
        print("\n" + "="*50)
        print("SCHEDULER DASHBOARD")
        print("="*50)
        print(f"Active Jobs: {len(jobs)}")
        print(f"Total Executions: {stats['total_executions']}")
        print(f"Success Rate: {stats['success_rate']:.1f}%")
        print(f"Avg Duration: {stats['avg_duration']:.2f}s")
        print("\nNext Runs:")
        for job in jobs:
            print(f"  - {job.job_func.__name__}: {job.next_run}")
        print("="*50 + "\n")

dashboard = SchedulerDashboard()

def monitored_job_with_dashboard(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        status = 'success'
        
        try:
            return func(*args, **kwargs)
        except Exception as e:
            status = 'failed'
            raise
        finally:
            duration = time.time() - start_time
            dashboard.record_execution(func.__name__, duration, status)
    
    return wrapper

# Afficher dashboard toutes les heures
schedule.every().hour.do(dashboard.show_dashboard)


[OK] NOTIFICATIONS ET ALERTES

# === Email notifications ===
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class EmailNotifier:
    def __init__(self, smtp_host, smtp_port, username, password):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
        self.username = username
        self.password = password
    
    def send_alert(self, subject, body, to_email):
        msg = MIMEMultipart()
        msg['From'] = self.username
        msg['To'] = to_email
        msg['Subject'] = subject
        
        msg.attach(MIMEText(body, 'plain'))
        
        try:
            with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
                server.starttls()
                server.login(self.username, self.password)
                server.send_message(msg)
            logging.info(f"Email envoyé: {subject}")
        except Exception as e:
            logging.error(f"Erreur envoi email: {e}")

notifier = EmailNotifier('smtp.gmail.com', 587, 'user@gmail.com', 'password')

def job_with_notification():
    try:
        # Travail
        result = do_important_task()
        notifier.send_alert(
            "Job Success",
            f"Task completed successfully: {result}",
            "admin@example.com"
        )
    except Exception as e:
        notifier.send_alert(
            "Job Failed",
            f"Task failed with error: {e}",
            "admin@example.com"
        )

schedule.every().day.at("10:00").do(job_with_notification)

# === Slack notifications ===
import requests

class SlackNotifier:
    def __init__(self, webhook_url):
        self.webhook_url = webhook_url
    
    def send_message(self, text, channel=None):
        payload = {'text': text}
        if channel:
            payload['channel'] = channel
        
        try:
            response = requests.post(self.webhook_url, json=payload)
            response.raise_for_status()
        except Exception as e:
            logging.error(f"Erreur Slack: {e}")

slack = SlackNotifier('https://hooks.slack.com/services/YOUR/WEBHOOK/URL')

def job_with_slack():
    slack.send_message("[RAPIDE] Job démarré")
    try:
        # Travail
        result = do_task()
        slack.send_message(f"[OK] Job terminé: {result}")
    except Exception as e:
        slack.send_message(f"[X] Job échoué: {e}")

schedule.every().hour.do(job_with_slack)

# === Discord webhook ===
class DiscordNotifier:
    def __init__(self, webhook_url):
        self.webhook_url = webhook_url
    
    def send_embed(self, title, description, color=0x00ff00):
        payload = {
            'embeds': [{
                'title': title,
                'description': description,
                'color': color,
                'timestamp': datetime.utcnow().isoformat()
            }]
        }
        
        requests.post(self.webhook_url, json=payload)

discord = DiscordNotifier('https://discord.com/api/webhooks/...')

def job_with_discord():
    discord.send_embed(
        "Scheduler Status",
        f"Job executed at {datetime.now()}",
        color=0x00ff00  # Green
    )

# === Telegram bot ===
class TelegramNotifier:
    def __init__(self, bot_token, chat_id):
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.api_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    
    def send_message(self, text):
        payload = {
            'chat_id': self.chat_id,
            'text': text,
            'parse_mode': 'HTML'
        }
        
        try:
            requests.post(self.api_url, json=payload)
        except Exception as e:
            logging.error(f"Erreur Telegram: {e}")

telegram = TelegramNotifier('BOT_TOKEN', 'CHAT_ID')

def critical_job():
    try:
        result = critical_operation()
        telegram.send_message(f"[OK] <b>Opération critique réussie</b>\n{result}")
    except Exception as e:
        telegram.send_message(f"[ALERTE] <b>ERREUR CRITIQUE</b>\n{e}")


[OK] BEST PRACTICES ET ANTI-PATTERNS

# === [OK] BONNES PRATIQUES ===

# 1. Utiliser des fonctions courtes et focalisées
def good_job():
    """Tâche bien définie et testable"""
    data = fetch_data()
    processed = process_data(data)
    save_results(processed)

schedule.every().hour.do(good_job)

# 2. Gérer les erreurs proprement
def safe_job():
    try:
        risky_operation()
    except SpecificException as e:
        logging.error(f"Erreur spécifique: {e}")
        send_alert(e)
    except Exception as e:
        logging.exception("Erreur inattendue")
    finally:
        cleanup()

# 3. Logger les exécutions
def logged_job():
    logging.info(f"Job démarré à {datetime.now()}")
    result = do_work()
    logging.info(f"Job terminé: {result}")
    return result

# 4. Utiliser des tags pour l'organisation
schedule.every().day.do(backup).tag('critical', 'daily')
schedule.every().hour.do(monitor).tag('monitoring', 'health')

# 5. Documenter les jobs
def documented_job():
    """
    Synchronise les données avec l'API externe.
    
    Exécution: Toutes les heures
    Dépendances: API externe, base de données
    Durée moyenne: 2-3 minutes
    Criticité: Moyenne
    """
    pass

# 6. Séparer configuration et code
# config.py
JOBS_CONFIG = {
    'backup': {
        'schedule': 'daily',
        'time': '02:00',
        'enabled': True
    },
    'sync': {
        'schedule': 'hourly',
        'enabled': True
    }
}

# 7. Timeout pour jobs longs
from timeout_decorator import timeout

@timeout(300)  # 5 minutes max
def long_job():
    slow_operation()

# === [X] ANTI-PATTERNS ===

# [X] 1. Jobs bloquants sans threading
def blocking_job():
    time.sleep(300)  # Bloque tout le scheduler!

# [OK] Solution
def non_blocking_job():
    threading.Thread(target=time.sleep, args=(300,)).start()

# [X] 2. Pas de gestion d'erreur
def bad_job():
    result = risky_api_call()  # Peut crasher tout le scheduler
    return result

# [X] 3. Jobs trop complexes
def monster_job():
    # 200 lignes de code...
    # Difficile à tester et maintenir
    pass

# [OK] Solution: Décomposer
def step1(): pass
def step2(): pass
def step3(): pass

def good_job():
    step1()
    step2()
    step3()

# [X] 4. State global mutable
counter = 0

def bad_stateful_job():
    global counter
    counter += 1  # Dangereux en multi-threading

# [OK] Solution: Utiliser une classe
class StatefulJob:
    def __init__(self):
        self.counter = 0
        self.lock = threading.Lock()
    
    def run(self):
        with self.lock:
            self.counter += 1

# [X] 5. Ignorer le temps d'exécution
schedule.every(5).seconds.do(slow_job)  # Si slow_job prend 10s?

# [OK] Solution: Monitoring
def timed_job():
    start = time.time()
    slow_job()
    duration = time.time() - start
    if duration > 5:
        logging.warning(f"Job trop long: {duration}s")

# [X] 6. Pas de clean shutdown
while True:
    schedule.run_pending()
    time.sleep(1)
    # Ctrl+C laisse des jobs en cours

# [OK] Solution: Graceful shutdown
try:
    while True:
        schedule.run_pending()
        time.sleep(1)
except KeyboardInterrupt:
    logging.info("Arrêt gracieux...")
    cleanup_resources()


[OK] PERFORMANCE ET OPTIMISATION

# === Éviter les jobs qui se chevauchent ===
from threading import Lock

job_lock = Lock()

def exclusive_job():
    if not job_lock.acquire(blocking=False):
        logging.warning("Job déjà en cours, skip")
        return
    
    try:
        do_work()
    finally:
        job_lock.release()

schedule.every(5).minutes.do(exclusive_job)

# === Limiter la concurrence ===
from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=3)

def concurrent_job():
    # Maximum 3 instances en parallèle
    future = executor.submit(do_heavy_work)
    return future

schedule.every(10).seconds.do(concurrent_job)

# === Rate limiting ===
from ratelimit import limits, sleep_and_retry

@sleep_and_retry
@limits(calls=10, period=60)  # Max 10 appels par minute
def rate_limited_job():
    api_call()

schedule.every(5).seconds.do(rate_limited_job)

# === Batch processing ===
batch_queue = []

def add_to_batch(item):
    batch_queue.append(item)

def process_batch():
    if not batch_queue:
        return
    
    # Traiter tous d'un coup
    batch = batch_queue.copy()
    batch_queue.clear()
    
    process_items(batch)

# Accumuler
schedule.every(1).second.do(lambda: add_to_batch(get_item()))

# Traiter par batch toutes les 5 minutes
schedule.every(5).minutes.do(process_batch)

# === Cache pour éviter travail répétitif ===
from functools import lru_cache
from datetime import datetime, timedelta

@lru_cache(maxsize=128)
def cached_expensive_operation(date_key):
    # Opération coûteuse
    return expensive_calculation()

def smart_job():
    # Utiliser date comme clé de cache
    today = datetime.now().date()
    result = cached_expensive_operation(today)
    return result

schedule.every().hour.do(smart_job)


[OK] EXEMPLES COMPLETS DE PRODUCTION

# === Application complète ===
import schedule
import time
import logging
import signal
import sys
from pathlib import Path
from datetime import datetime

# Configuration logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('scheduler.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

# ============ JOBS ============

def backup_database():
    """Backup quotidien de la base de données"""
    logger.info("Début backup base de données")
    try:
        # Logique de backup
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_file = f"backup_{timestamp}.sql"
        # pg_dump, mysqldump, etc.
        logger.info(f"Backup créé: {backup_file}")
    except Exception as e:
        logger.error(f"Erreur backup: {e}")
        send_alert(f"Backup failed: {e}")

def sync_data():
    """Synchronisation avec API externe"""
    logger.info("Début synchronisation")
    try:
        # API sync logic
        response = api.sync()
        logger.info(f"Sync terminée: {response}")
    except Exception as e:
        logger.error(f"Erreur sync: {e}")

def generate_report():
    """Génération du rapport quotidien"""
    logger.info("Génération rapport")
    try:
        report = create_daily_report()
        send_report(report)
        logger.info("Rapport envoyé")
    except Exception as e:
        logger.error(f"Erreur rapport: {e}")

def health_check():
    """Vérification santé système"""
    logger.info("Health check")
    # Vérifier services, disque, mémoire, etc.
    status = check_system_health()
    if not status['healthy']:
        send_alert(f"System unhealthy: {status}")

def cleanup_old_files():
    """Nettoyage fichiers temporaires"""
    logger.info("Nettoyage fichiers")
    temp_dir = Path('/tmp/myapp')
    cutoff = datetime.now() - timedelta(days=7)
    
    for file in temp_dir.glob('*'):
        if datetime.fromtimestamp(file.stat().st_mtime) < cutoff:
            file.unlink()
            logger.info(f"Supprimé: {file}")

# ============ CONFIGURATION ============

def setup_schedule():
    """Configuration de tous les jobs"""
    
    # Backups
    schedule.every().day.at("02:00").do(backup_database).tag('backup', 'critical')
    
    # Synchronisation
    schedule.every(15).minutes.do(sync_data).tag('sync', 'api')
    
    # Rapports
    schedule.every().day.at("09:00").do(generate_report).tag('reports', 'daily')
    schedule.every().friday.at("17:00").do(generate_weekly_report).tag('reports', 'weekly')
    
    # Monitoring
    schedule.every(5).minutes.do(health_check).tag('monitoring', 'health')
    
    # Maintenance
    schedule.every().day.at("03:00").do(cleanup_old_files).tag('maintenance')
    
    logger.info(f"Planifié {len(schedule.get_jobs())} jobs")

# ============ SCHEDULER ============

class ProductionScheduler:
    def __init__(self):
        self.is_running = True
        self.setup_signal_handlers()
    
    def setup_signal_handlers(self):
        """Configuration gestion signaux"""
        signal.signal(signal.SIGINT, self.shutdown)
        signal.signal(signal.SIGTERM, self.shutdown)
    
    def shutdown(self, signum, frame):
        """Arrêt gracieux"""
        logger.info(f"Signal {signum} reçu, arrêt...")
        self.is_running = False
    
    def run(self):
        """Boucle principale"""
        logger.info("Démarrage scheduler")
        setup_schedule()
        
        while self.is_running:
            try:
                schedule.run_pending()
                
                # Sleep intelligent
                idle = schedule.idle_seconds()
                if idle and idle > 0:
                    time.sleep(min(idle, 60))
                else:
                    time.sleep(1)
                    
            except Exception as e:
                logger.exception(f"Erreur dans loop: {e}")
                time.sleep(5)
        
        logger.info("Scheduler arrêté proprement")
        sys.exit(0)

# ============ MAIN ============

if __name__ == '__main__':
    scheduler = ProductionScheduler()
    scheduler.run()


[OK] RESSOURCES ET DOCUMENTATION

# Documentation officielle
# https://schedule.readthedocs.io/

# GitHub
# https://github.com/dbader/schedule

# PyPI
# https://pypi.org/project/schedule/

# Alternatives
# - APScheduler: https://apscheduler.readthedocs.io/
# - Celery Beat: https://docs.celeryproject.org/en/stable/userguide/periodic-tasks.html
# - Python-crontab: https://gitlab.com/doctoryes/python-crontab

# Tutoriels
# - Real Python: Scheduling Python Tasks
# - DataCamp: Python Task Scheduling


[OK] CHANGELOG ET VERSIONS

# Version 1.2.0 (dernière stable)
# - Support Python 3.7+
# - Tags pour organiser jobs
# - Amélioration performance
# - Meilleure gestion erreurs

# Installation version spécifique
pip install schedule==1.2.0