# Fichier: python_cheats/cheatsheets/typer.txt
# Cheatsheet Typer - Guide Complet pour CLI Python


[OK] INTRODUCTION À TYPER

# Typer est une bibliothèque moderne pour créer des CLI (Command Line Interface)
# Basé sur les type hints Python et construit sur Click
# Rend la création de CLI aussi simple que d'écrire des fonctions Python

# Avantages:
# [OK] Syntaxe intuitive et simple
# [OK] Validation automatique des types
# [OK] Génération automatique de l'aide
# [OK] Auto-complétion shell
# [OK] Tests faciles
# [OK] Documentation automatique


[OK] INSTALLATION

# Installation simple
pip install typer

# Avec support complet (recommandé)
pip install "typer[all]"

# Avec shell completion
pip install typer[all]

# Installation spécifique
pip install typer==0.9.0

# Vérifier installation
python -c "import typer; print(typer.__version__)"


[OK] PREMIER PROGRAMME - HELLO WORLD

# === hello.py ===
import typer

def main():
    """Programme simple qui dit bonjour."""
    typer.echo("Hello World!")

if __name__ == "__main__":
    typer.run(main)

# Exécuter
# python hello.py
# Output: Hello World!


# === Avec paramètre ===
import typer

def main(name: str):
    """Dit bonjour à quelqu'un.
    
    Args:
        name: Le nom de la personne
    """
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(main)

# Exécuter
# python hello.py Alice
# Output: Hello Alice!


# === Avec paramètre optionnel ===
import typer

def main(name: str = "World"):
    """Dit bonjour à quelqu'un."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(main)

# Exécuter
# python hello.py
# Output: Hello World!
# python hello.py Alice
# Output: Hello Alice!


[OK] TYPER APP - APPLICATION COMPLÈTE

# === app_simple.py ===
import typer

app = typer.Typer()

@app.command()
def hello(name: str):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

@app.command()
def goodbye(name: str):
    """Dit au revoir."""
    typer.echo(f"Goodbye {name}!")

if __name__ == "__main__":
    app()

# Exécuter
# python app_simple.py hello Alice
# Output: Hello Alice!
# python app_simple.py goodbye Bob
# Output: Goodbye Bob!


# === Avec description de l'app ===
import typer

app = typer.Typer(
    help="Mon super CLI pour saluer les gens.",
    add_completion=True
)

@app.command()
def hello(name: str):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    app()


[OK] ARGUMENTS - PARAMÈTRES POSITIONNELS

# === Arguments simples ===
import typer

def copy(source: str, dest: str):
    """Copie un fichier.
    
    Args:
        source: Fichier source
        dest: Fichier destination
    """
    typer.echo(f"Copying {source} to {dest}")

if __name__ == "__main__":
    typer.run(copy)

# Exécuter
# python copy.py file1.txt file2.txt
# Output: Copying file1.txt to file2.txt


# === Argument avec typer.Argument() ===
import typer
from typing_extensions import Annotated

def greet(
    name: Annotated[str, typer.Argument(help="Nom de la personne")]
):
    """Dit bonjour à quelqu'un."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(greet)


# === Argument avec valeur par défaut ===
import typer
from typing_extensions import Annotated

def greet(
    name: Annotated[str, typer.Argument()] = "World"
):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(greet)

# python greet.py          -> Hello World!
# python greet.py Alice    -> Hello Alice!


# === Argument avec description ===
import typer
from typing_extensions import Annotated

def process(
    filename: Annotated[
        str, 
        typer.Argument(
            help="Le fichier à traiter",
            show_default=False
        )
    ]
):
    """Traite un fichier."""
    typer.echo(f"Processing {filename}")

if __name__ == "__main__":
    typer.run(process)


# === Arguments multiples (liste) ===
import typer
from typing import List
from typing_extensions import Annotated

def process_files(
    files: Annotated[List[str], typer.Argument(help="Fichiers à traiter")]
):
    """Traite plusieurs fichiers."""
    for file in files:
        typer.echo(f"Processing {file}")

if __name__ == "__main__":
    typer.run(process_files)

# python process.py file1.txt file2.txt file3.txt
# Output:
# Processing file1.txt
# Processing file2.txt
# Processing file3.txt


[OK] OPTIONS - PARAMÈTRES NOMMÉS

# === Option simple ===
import typer
from typing_extensions import Annotated

def greet(
    name: Annotated[str, typer.Option(help="Nom de la personne")] = "World"
):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(greet)

# python greet.py --name Alice
# Output: Hello Alice!


# === Option avec nom court ===
import typer
from typing_extensions import Annotated

def greet(
    name: Annotated[str, typer.Option("-n", "--name")] = "World"
):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(greet)

# python greet.py -n Alice
# python greet.py --name Alice


# === Options multiples ===
import typer
from typing_extensions import Annotated

def create_user(
    username: Annotated[str, typer.Option("--username", "-u")],
    email: Annotated[str, typer.Option("--email", "-e")],
    age: Annotated[int, typer.Option("--age", "-a")] = 18
):
    """Crée un utilisateur."""
    typer.echo(f"Creating user: {username}, {email}, age {age}")

if __name__ == "__main__":
    typer.run(create_user)

# python create.py -u alice -e alice@example.com -a 25
# python create.py --username bob --email bob@example.com


# === Option booléenne (flag) ===
import typer
from typing_extensions import Annotated

def delete_file(
    filename: str,
    force: Annotated[bool, typer.Option("--force", "-f")] = False
):
    """Supprime un fichier."""
    if force:
        typer.echo(f"Force deleting {filename}")
    else:
        typer.echo(f"Deleting {filename}")

if __name__ == "__main__":
    typer.run(delete_file)

# python delete.py file.txt
# python delete.py file.txt --force
# python delete.py file.txt -f


# === Flag avec is_flag ===
import typer
from typing_extensions import Annotated

def verbose_command(
    verbose: Annotated[bool, typer.Option("--verbose", "-v", is_flag=True)] = False
):
    """Commande avec mode verbose."""
    if verbose:
        typer.echo("Mode verbose activé!")
    typer.echo("Executing command...")

if __name__ == "__main__":
    typer.run(verbose_command)


# === Option compteur (--verbose -vvv) ===
import typer
from typing_extensions import Annotated

def debug_command(
    verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0
):
    """Commande avec niveaux de verbosité."""
    typer.echo(f"Verbosity level: {verbose}")
    if verbose >= 3:
        typer.echo("DEBUG mode")
    elif verbose >= 2:
        typer.echo("INFO mode")
    elif verbose >= 1:
        typer.echo("WARNING mode")

if __name__ == "__main__":
    typer.run(debug_command)

# python debug.py -v        -> Verbosity: 1
# python debug.py -vv       -> Verbosity: 2
# python debug.py -vvv      -> Verbosity: 3
# python debug.py --verbose --verbose -> Verbosity: 2


# === Option avec prompt ===
import typer
from typing_extensions import Annotated

def login(
    username: Annotated[str, typer.Option(prompt=True)],
    password: Annotated[str, typer.Option(prompt=True, hide_input=True)]
):
    """Login utilisateur."""
    typer.echo(f"Logging in as {username}")

if __name__ == "__main__":
    typer.run(login)

# python login.py
# Username: alice
# Password: ****
# Logging in as alice


# === Option avec prompt personnalisé ===
import typer
from typing_extensions import Annotated

def signup(
    username: Annotated[str, typer.Option(prompt="Choose a username")],
    email: Annotated[str, typer.Option(prompt="Your email address")]
):
    """Inscription utilisateur."""
    typer.echo(f"Signing up: {username} ({email})")

if __name__ == "__main__":
    typer.run(signup)


# === Option avec confirmation ===
import typer
from typing_extensions import Annotated

def delete_account(
    username: str,
    confirm: Annotated[bool, typer.Option(
        prompt="Are you sure you want to delete?",
        confirmation_prompt=True
    )] = False
):
    """Supprime un compte."""
    if confirm:
        typer.echo(f"Deleting account: {username}")
    else:
        typer.echo("Cancelled")

if __name__ == "__main__":
    typer.run(delete_account)


# === Option avec valeurs limitées (choices) ===
import typer
from typing_extensions import Annotated
from enum import Enum

class Environment(str, Enum):
    dev = "development"
    staging = "staging"
    prod = "production"

def deploy(
    env: Annotated[Environment, typer.Option(case_sensitive=False)]
):
    """Déploie sur un environnement."""
    typer.echo(f"Deploying to {env.value}")

if __name__ == "__main__":
    typer.run(deploy)

# python deploy.py --env dev
# python deploy.py --env production


# === Option avec callback de validation ===
import typer
from typing_extensions import Annotated

def validate_port(value: int):
    """Valide que le port est dans la plage correcte."""
    if value < 1 or value > 65535:
        raise typer.BadParameter("Port must be between 1 and 65535")
    return value

def start_server(
    port: Annotated[int, typer.Option(callback=validate_port)] = 8000
):
    """Démarre le serveur."""
    typer.echo(f"Starting server on port {port}")

if __name__ == "__main__":
    typer.run(start_server)


# === Option avec liste de valeurs ===
import typer
from typing import List
from typing_extensions import Annotated

def process(
    tags: Annotated[List[str], typer.Option("--tag", "-t")] = None
):
    """Traite avec des tags."""
    if tags:
        typer.echo(f"Tags: {', '.join(tags)}")
    else:
        typer.echo("No tags")

if __name__ == "__main__":
    typer.run(process)

# python process.py --tag python --tag cli --tag typer
# Output: Tags: python, cli, typer


# === Option requise ===
import typer
from typing_extensions import Annotated

def send_email(
    recipient: Annotated[str, typer.Option(..., help="Email du destinataire")],
    subject: Annotated[str, typer.Option(...)] = None
):
    """Envoie un email."""
    typer.echo(f"Sending email to {recipient}: {subject}")

if __name__ == "__main__":
    typer.run(send_email)

# ... signifie que l'option est requise


# === Option avec variable d'environnement ===
import typer
from typing_extensions import Annotated

def connect_db(
    host: Annotated[str, typer.Option(envvar="DB_HOST")] = "localhost",
    port: Annotated[int, typer.Option(envvar="DB_PORT")] = 5432
):
    """Se connecte à la base de données."""
    typer.echo(f"Connecting to {host}:{port}")

if __name__ == "__main__":
    typer.run(connect_db)

# export DB_HOST=prod.db.com
# export DB_PORT=3306
# python connect.py
# Output: Connecting to prod.db.com:3306


# === Option avec plusieurs variables d'environnement ===
import typer
from typing_extensions import Annotated

def api_call(
    token: Annotated[str, typer.Option(envvar=["API_TOKEN", "AUTH_TOKEN"])]
):
    """Appelle l'API."""
    typer.echo(f"Using token: {token[:10]}...")

if __name__ == "__main__":
    typer.run(api_call)


[OK] TYPES DE DONNÉES

# === Type str (chaîne) ===
def greet(name: str):
    typer.echo(f"Hello {name}")


# === Type int (entier) ===
def repeat(message: str, count: int):
    for _ in range(count):
        typer.echo(message)


# === Type float (décimal) ===
def calculate_price(price: float, tax_rate: float = 0.2):
    total = price * (1 + tax_rate)
    typer.echo(f"Total: ${total:.2f}")


# === Type bool (booléen) ===
from typing_extensions import Annotated

def run(
    verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False
):
    if verbose:
        typer.echo("Verbose mode ON")


# === Type Path (fichier/dossier) ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def read_file(
    file: Annotated[Path, typer.Argument(exists=True, file_okay=True, dir_okay=False)]
):
    """Lit un fichier."""
    content = file.read_text()
    typer.echo(content)

if __name__ == "__main__":
    typer.run(read_file)


# === Path avec validation ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def process_dir(
    directory: Annotated[Path, typer.Argument(
        exists=True,           # Doit exister
        file_okay=False,       # Pas de fichier
        dir_okay=True,         # Seulement dossier
        readable=True,         # Doit être lisible
        resolve_path=True      # Résout le chemin complet
    )]
):
    """Traite un dossier."""
    typer.echo(f"Processing directory: {directory}")
    for file in directory.iterdir():
        typer.echo(f"  - {file.name}")

if __name__ == "__main__":
    typer.run(process_dir)


# === Type List (liste) ===
import typer
from typing import List
from typing_extensions import Annotated

def process_items(
    items: Annotated[List[str], typer.Argument()]
):
    """Traite plusieurs éléments."""
    for item in items:
        typer.echo(f"Processing: {item}")

if __name__ == "__main__":
    typer.run(process_items)


# === Type Tuple (tuple) ===
import typer
from typing import Tuple
from typing_extensions import Annotated

def create_user(
    user_info: Annotated[Tuple[str, int, str], typer.Option()]
):
    """Crée un utilisateur avec (nom, age, email)."""
    name, age, email = user_info
    typer.echo(f"User: {name}, {age} years old, {email}")

if __name__ == "__main__":
    typer.run(create_user)


# === Type Enum (énumération) ===
import typer
from enum import Enum
from typing_extensions import Annotated

class LogLevel(str, Enum):
    debug = "DEBUG"
    info = "INFO"
    warning = "WARNING"
    error = "ERROR"

def log(
    message: str,
    level: Annotated[LogLevel, typer.Option()] = LogLevel.info
):
    """Log un message."""
    typer.echo(f"[{level.value}] {message}")

if __name__ == "__main__":
    typer.run(log)

# python log.py "Test message" --level debug


# === Type DateTime ===
import typer
from datetime import datetime
from typing_extensions import Annotated

def schedule(
    task: str,
    when: Annotated[datetime, typer.Option(formats=["%Y-%m-%d", "%Y-%m-%d %H:%M"])]
):
    """Planifie une tâche."""
    typer.echo(f"Task '{task}' scheduled for {when}")

if __name__ == "__main__":
    typer.run(schedule)

# python schedule.py "Backup" --when 2024-12-25
# python schedule.py "Meeting" --when "2024-12-25 14:30"


# === Type UUID ===
import typer
from uuid import UUID
from typing_extensions import Annotated

def get_user(
    user_id: Annotated[UUID, typer.Argument()]
):
    """Récupère un utilisateur par UUID."""
    typer.echo(f"Getting user: {user_id}")

if __name__ == "__main__":
    typer.run(get_user)

# python get_user.py 123e4567-e89b-12d3-a456-426614174000


# === Type Optional ===
import typer
from typing import Optional
from typing_extensions import Annotated

def greet(
    name: str,
    title: Annotated[Optional[str], typer.Option()] = None
):
    """Salue quelqu'un avec un titre optionnel."""
    if title:
        typer.echo(f"Hello {title} {name}")
    else:
        typer.echo(f"Hello {name}")

if __name__ == "__main__":
    typer.run(greet)


[OK] COMMANDES MULTIPLES

# === Application avec plusieurs commandes ===
import typer

app = typer.Typer()

@app.command()
def create(name: str):
    """Crée un nouvel élément."""
    typer.echo(f"Creating {name}")

@app.command()
def delete(name: str):
    """Supprime un élément."""
    typer.echo(f"Deleting {name}")

@app.command()
def list():
    """Liste tous les éléments."""
    typer.echo("Listing all items")

if __name__ == "__main__":
    app()

# python app.py create item1
# python app.py delete item2
# python app.py list


# === Commande avec nom personnalisé ===
import typer

app = typer.Typer()

@app.command("ls")
def list_items():
    """Liste les éléments."""
    typer.echo("Listing items")

@app.command("rm")
def remove_item(name: str):
    """Supprime un élément."""
    typer.echo(f"Removing {name}")

if __name__ == "__main__":
    app()

# python app.py ls
# python app.py rm item1


# === Sous-commandes (sub-apps) ===
import typer

app = typer.Typer()
user_app = typer.Typer()
project_app = typer.Typer()

app.add_typer(user_app, name="user", help="Gestion des utilisateurs")
app.add_typer(project_app, name="project", help="Gestion des projets")

@user_app.command("create")
def create_user(username: str):
    """Crée un utilisateur."""
    typer.echo(f"Creating user: {username}")

@user_app.command("delete")
def delete_user(username: str):
    """Supprime un utilisateur."""
    typer.echo(f"Deleting user: {username}")

@project_app.command("create")
def create_project(name: str):
    """Crée un projet."""
    typer.echo(f"Creating project: {name}")

@project_app.command("list")
def list_projects():
    """Liste les projets."""
    typer.echo("Listing projects")

if __name__ == "__main__":
    app()

# python app.py user create alice
# python app.py user delete bob
# python app.py project create myproject
# python app.py project list


# === Structure avec groupes ===
import typer

app = typer.Typer()

# Groupe database
db_app = typer.Typer(help="Commandes de base de données")
app.add_typer(db_app, name="db")

@db_app.command()
def migrate():
    """Lance les migrations."""
    typer.echo("Running migrations...")

@db_app.command()
def seed():
    """Remplit la base avec des données."""
    typer.echo("Seeding database...")

# Groupe cache
cache_app = typer.Typer(help="Commandes de cache")
app.add_typer(cache_app, name="cache")

@cache_app.command()
def clear():
    """Vide le cache."""
    typer.echo("Clearing cache...")

@cache_app.command()
def stats():
    """Statistiques du cache."""
    typer.echo("Cache statistics...")

if __name__ == "__main__":
    app()

# python app.py db migrate
# python app.py cache clear


[OK] CALLBACK - COMMANDES PRINCIPALES

# === Callback pour options globales ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.callback()
def main(
    verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False,
    config: Annotated[str, typer.Option()] = "config.yml"
):
    """
    Mon CLI avec options globales.
    
    Les options --verbose et --config sont disponibles pour toutes les commandes.
    """
    if verbose:
        typer.echo("Verbose mode enabled")
    typer.echo(f"Using config: {config}")

@app.command()
def run():
    """Lance l'application."""
    typer.echo("Running application")

@app.command()
def test():
    """Lance les tests."""
    typer.echo("Running tests")

if __name__ == "__main__":
    app()

# python app.py --verbose run
# python app.py --config prod.yml test


# === Callback avec contexte ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False
):
    """CLI avec contexte."""
    # Stocker la config dans le contexte
    ctx.ensure_object(dict)
    ctx.obj['verbose'] = verbose

@app.command()
def run(ctx: typer.Context):
    """Lance l'application."""
    if ctx.obj.get('verbose'):
        typer.echo("Verbose mode ON")
    typer.echo("Running...")

if __name__ == "__main__":
    app()


# === Callback sans commandes ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main(
    ctx: typer.Context,
    version: Annotated[bool, typer.Option("--version")] = False
):
    """Mon CLI."""
    if version:
        typer.echo("Version 1.0.0")
        raise typer.Exit()
    
    if ctx.invoked_subcommand is None:
        typer.echo("No command specified. Use --help")

@app.command()
def run():
    """Lance l'application."""
    typer.echo("Running...")

if __name__ == "__main__":
    app()


[OK] SORTIE ET AFFICHAGE

# === typer.echo() - Affichage simple ===
import typer

def main():
    typer.echo("Message simple")
    typer.echo("Message avec couleur", color=True)
    typer.echo("Message sur stderr", err=True)

if __name__ == "__main__":
    typer.run(main)


# === Couleurs ===
import typer

def colors():
    """Affiche avec des couleurs."""
    typer.secho("Success!", fg=typer.colors.GREEN, bold=True)
    typer.secho("Warning!", fg=typer.colors.YELLOW)
    typer.secho("Error!", fg=typer.colors.RED, bold=True)
    typer.secho("Info", fg=typer.colors.BLUE)
    typer.secho("Text with background", bg=typer.colors.GREEN)

if __name__ == "__main__":
    typer.run(colors)


# === Style personnalisé ===
import typer

def styled():
    """Affiche avec style."""
    typer.secho("Bold text", bold=True)
    typer.secho("Dim text", dim=True)
    typer.secho("Underlined", underline=True)
    typer.secho("Blinking", blink=True)
    typer.secho("Reversed", reverse=True)

if __name__ == "__main__":
    typer.run(styled)


# === typer.style() - Style avancé ===
import typer

def advanced_style():
    """Style avancé."""
    text = typer.style(
        "Custom styled text",
        fg=typer.colors.CYAN,
        bg=typer.colors.BLACK,
        bold=True,
        underline=True
    )
    typer.echo(text)

if __name__ == "__main__":
    typer.run(advanced_style)


# === Progress bar ===
import typer
import time

def process_items():
    """Traite des éléments avec barre de progression."""
    items = range(100)
    
    with typer.progressbar(items, label="Processing") as progress:
        for item in progress:
            time.sleep(0.01)  # Simule du travail
    
    typer.echo("Done!")

if __name__ == "__main__":
    typer.run(process_items)


# === Progress bar personnalisée ===
import typer
import time

def process_custom():
    """Progress bar personnalisée."""
    items = range(50)
    
    with typer.progressbar(
        items,
        label="Downloading",
        length=50,
        show_eta=True,
        show_percent=True,
        show_pos=True
    ) as progress:
        for item in progress:
            time.sleep(0.05)

if __name__ == "__main__":
    typer.run(process_custom)


# === Spinner (indicateur de chargement) ===
import typer
import time

def long_task():
    """Tâche longue avec spinner."""
    with typer.progressbar(
        length=100,
        label="Processing"
    ) as progress:
        for i in range(100):
            time.sleep(0.02)
            progress.update(1)
    
    typer.secho("[OK] Done!", fg=typer.colors.GREEN)

if __name__ == "__main__":
    typer.run(long_task)


# === Affichage de tableaux ===
import typer

def show_table():
    """Affiche un tableau."""
    data = [
        ["Alice", "25", "alice@example.com"],
        ["Bob", "30", "bob@example.com"],
        ["Charlie", "35", "charlie@example.com"]
    ]
    
    # En-tête
    typer.secho("Name       Age  Email", bold=True)
    typer.echo("-" * 50)
    
    # Données
    for row in data:
        typer.echo(f"{row[0]:<10} {row[1]:<4} {row[2]}")

if __name__ == "__main__":
    typer.run(show_table)


# === Pagination ===
import typer

def show_long_content():
    """Affiche un long contenu avec pagination."""
    content = "\n".join([f"Line {i}" for i in range(100)])
    typer.echo_via_pager(content)

if __name__ == "__main__":
    typer.run(show_long_content)


# === Clear screen ===
import typer

def clear_screen():
    """Efface l'écran."""
    typer.clear()
    typer.echo("Screen cleared!")

if __name__ == "__main__":
    typer.run(clear_screen)


# === Éditer du texte ===
import typer

def edit_text():
    """Ouvre un éditeur pour modifier du texte."""
    initial_text = "# Configuration\nkey = value"
    edited_text = typer.edit(initial_text)
    
    if edited_text:
        typer.echo("Modified text:")
        typer.echo(edited_text)
    else:
        typer.echo("No changes made")

if __name__ == "__main__":
    typer.run(edit_text)


# === Lancer un programme externe ===
import typer

def open_browser():
    """Ouvre un URL dans le navigateur."""
    typer.launch("https://example.com")

def open_file():
    """Ouvre un fichier avec l'application par défaut."""
    typer.launch("document.pdf")

if __name__ == "__main__":
    typer.run(open_browser)


[OK] CONFIRMATION ET PROMPTS

# === Demander confirmation ===
import typer

def delete_database():
    """Supprime la base de données."""
    if typer.confirm("Are you sure you want to delete the database?"):
        typer.echo("Deleting database...")
        # Code de suppression
    else:
        typer.echo("Aborted")

if __name__ == "__main__":
    typer.run(delete_database)


# === Confirmation avec abort ===
import typer

def dangerous_action():
    """Action dangereuse."""
    typer.confirm("Delete all files?", abort=True)
    typer.echo("Deleting all files...")

if __name__ == "__main__":
    typer.run(dangerous_action)


# === Prompt simple ===
import typer

def get_name():
    """Demande le nom à l'utilisateur."""
    name = typer.prompt("What's your name?")
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(get_name)


# === Prompt avec valeur par défaut ===
import typer

def configure():
    """Configuration avec valeurs par défaut."""
    host = typer.prompt("Host", default="localhost")
    port = typer.prompt("Port", default=8000, type=int)
    typer.echo(f"Connecting to {host}:{port}")

if __name__ == "__main__":
    typer.run(configure)


# === Prompt caché (mot de passe) ===
import typer

def login():
    """Login avec mot de passe caché."""
    username = typer.prompt("Username")
    password = typer.prompt("Password", hide_input=True)
    typer.echo(f"Logging in as {username}")

if __name__ == "__main__":
    typer.run(login)


# === Prompt avec confirmation ===
import typer

def set_password():
    """Définit un mot de passe avec confirmation."""
    password = typer.prompt(
        "New password",
        hide_input=True,
        confirmation_prompt=True
    )
    typer.echo("Password set successfully!")

if __name__ == "__main__":
    typer.run(set_password)


# === Prompt avec validation ===
import typer

def validate_age(value: str) -> int:
    """Valide l'âge."""
    age = int(value)
    if age < 0 or age > 120:
        raise typer.BadParameter("Age must be between 0 and 120")
    return age

def register():
    """Inscription avec validation."""
    age = typer.prompt("Your age", value_proc=validate_age)
    typer.echo(f"Age: {age}")

if __name__ == "__main__":
    typer.run(register)


# === Prompt avec choix ===
import typer

def choose_option():
    """Choisit une option."""
    choices = ["Option 1", "Option 2", "Option 3"]
    
    typer.echo("Available options:")
    for i, choice in enumerate(choices, 1):
        typer.echo(f"{i}. {choice}")
    
    selection = typer.prompt("Choose an option (1-3)", type=int)
    
    if 1 <= selection <= len(choices):
        typer.echo(f"You chose: {choices[selection-1]}")
    else:
        typer.echo("Invalid choice")

if __name__ == "__main__":
    typer.run(choose_option)


[OK] GESTION DES ERREURS

# === Quitter avec code de sortie ===
import typer

def check_file():
    """Vérifie un fichier."""
    import os
    
    if not os.path.exists("config.yml"):
        typer.echo("Error: config.yml not found", err=True)
        raise typer.Exit(code=1)
    
    typer.echo("Config file found!")

if __name__ == "__main__":
    typer.run(check_file)


# === Abort (quitter immédiatement) ===
import typer

def check_requirements():
    """Vérifie les prérequis."""
    if not some_condition:
        typer.echo("Fatal error: requirements not met")
        raise typer.Abort()

if __name__ == "__main__":
    typer.run(check_requirements)


# === BadParameter (paramètre invalide) ===
import typer
from typing_extensions import Annotated

def validate_email(value: str) -> str:
    """Valide une adresse email."""
    if "@" not in value:
        raise typer.BadParameter("Invalid email address")
    return value

def send_email(
    email: Annotated[str, typer.Option(callback=validate_email)]
):
    """Envoie un email."""
    typer.echo(f"Sending to {email}")

if __name__ == "__main__":
    typer.run(send_email)


# === Gestion d'exceptions ===
import typer

app = typer.Typer()

@app.command()
def divide(a: int, b: int):
    """Divise deux nombres."""
    try:
        result = a / b
        typer.echo(f"Result: {result}")
    except ZeroDivisionError:
        typer.secho("Error: Division by zero!", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1)
    except Exception as e:
        typer.secho(f"Unexpected error: {e}", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=2)

if __name__ == "__main__":
    app()


# === Try/except global ===
import typer

app = typer.Typer()

@app.command()
def risky_operation():
    """Opération risquée."""
    # Code qui peut lever une exception
    raise ValueError("Something went wrong")

if __name__ == "__main__":
    try:
        app()
    except Exception as e:
        typer.secho(f"Fatal error: {e}", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1)


[OK] FICHIERS ET CHEMINS

# === Lire un fichier ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def read_config(
    config_file: Annotated[Path, typer.Argument(
        exists=True,
        file_okay=True,
        dir_okay=False,
        readable=True
    )]
):
    """Lit un fichier de configuration."""
    content = config_file.read_text()
    typer.echo(f"Config:\n{content}")

if __name__ == "__main__":
    typer.run(read_config)


# === Écrire dans un fichier ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def write_log(
    log_file: Annotated[Path, typer.Option()],
    message: str
):
    """Écrit dans un fichier de log."""
    log_file.write_text(message)
    typer.secho(f"[OK] Logged to {log_file}", fg=typer.colors.GREEN)

if __name__ == "__main__":
    typer.run(write_log)


# === Vérifier existence de fichier ===
import typer
from pathlib import Path

def process_if_exists(filename: str):
    """Traite un fichier s'il existe."""
    path = Path(filename)
    
    if not path.exists():
        typer.secho(f"Error: {filename} not found", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1)
    
    if not path.is_file():
        typer.secho(f"Error: {filename} is not a file", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1)
    
    typer.echo(f"Processing {filename}")

if __name__ == "__main__":
    typer.run(process_if_exists)


# === Créer un dossier ===
import typer
from pathlib import Path

def create_project(name: str):
    """Crée un nouveau projet."""
    project_dir = Path(name)
    
    if project_dir.exists():
        typer.secho(f"Error: {name} already exists", fg=typer.colors.RED)
        raise typer.Exit(code=1)
    
    project_dir.mkdir(parents=True)
    (project_dir / "src").mkdir()
    (project_dir / "tests").mkdir()
    (project_dir / "README.md").write_text(f"# {name}\n")
    
    typer.secho(f"[OK] Project {name} created!", fg=typer.colors.GREEN)

if __name__ == "__main__":
    typer.run(create_project)


# === Lister les fichiers d'un dossier ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def list_files(
    directory: Annotated[Path, typer.Argument(
        exists=True,
        file_okay=False,
        dir_okay=True
    )] = Path(".")
):
    """Liste les fichiers d'un dossier."""
    typer.secho(f"Files in {directory}:", bold=True)
    
    for item in sorted(directory.iterdir()):
        if item.is_file():
            typer.echo(f"  [FICHIER] {item.name}")
        else:
            typer.echo(f"  [DOSSIER] {item.name}/")

if __name__ == "__main__":
    typer.run(list_files)


# === Copier un fichier ===
import typer
from pathlib import Path
import shutil

def copy_file(source: Path, dest: Path):
    """Copie un fichier."""
    if not source.exists():
        typer.secho(f"Error: {source} not found", fg=typer.colors.RED)
        raise typer.Exit(code=1)
    
    if dest.exists():
        if not typer.confirm(f"{dest} already exists. Overwrite?"):
            typer.echo("Aborted")
            raise typer.Exit()
    
    shutil.copy2(source, dest)
    typer.secho(f"[OK] Copied {source} to {dest}", fg=typer.colors.GREEN)

if __name__ == "__main__":
    typer.run(copy_file)


# === Travailler avec des fichiers temporaires ===
import typer
import tempfile
from pathlib import Path

def process_with_temp():
    """Traite avec un fichier temporaire."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
        tmp.write("Temporary data")
        tmp_path = tmp.name
    
    typer.echo(f"Created temp file: {tmp_path}")
    
    # Traitement
    content = Path(tmp_path).read_text()
    typer.echo(f"Content: {content}")
    
    # Nettoyage
    Path(tmp_path).unlink()
    typer.echo("Temp file deleted")

if __name__ == "__main__":
    typer.run(process_with_temp)


[OK] CONTEXT (CONTEXTE D'EXÉCUTION)

# === Utiliser le contexte ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    config: Annotated[str, typer.Option()] = "config.yml"
):
    """CLI avec contexte."""
    ctx.ensure_object(dict)
    ctx.obj['config'] = config
    ctx.obj['verbose'] = True

@app.command()
def run(ctx: typer.Context):
    """Lance avec config du contexte."""
    config = ctx.obj.get('config')
    verbose = ctx.obj.get('verbose')
    
    if verbose:
        typer.echo(f"Using config: {config}")
    typer.echo("Running...")

if __name__ == "__main__":
    app()


# === Accéder aux infos du contexte ===
import typer

app = typer.Typer()

@app.command()
def info(ctx: typer.Context):
    """Affiche les infos du contexte."""
    typer.echo(f"Command: {ctx.info_name}")
    typer.echo(f"Parent: {ctx.parent}")
    typer.echo(f"Params: {ctx.params}")

if __name__ == "__main__":
    app()


# === Passer des objets via le contexte ===
import typer
from dataclasses import dataclass

@dataclass
class Config:
    """Configuration de l'application."""
    host: str
    port: int
    debug: bool

app = typer.Typer()

@app.callback()
def main(ctx: typer.Context):
    """Initialise la config."""
    config = Config(host="localhost", port=8000, debug=False)
    ctx.obj = config

@app.command()
def run(ctx: typer.Context):
    """Lance avec la config."""
    config: Config = ctx.obj
    typer.echo(f"Starting server on {config.host}:{config.port}")
    if config.debug:
        typer.echo("Debug mode enabled")

if __name__ == "__main__":
    app()


[OK] TESTING (TESTS UNITAIRES)

# === Test avec CliRunner ===
# app.py
import typer

app = typer.Typer()

@app.command()
def hello(name: str):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    app()


# test_app.py
from typer.testing import CliRunner
from app import app

runner = CliRunner()

def test_hello():
    """Test de la commande hello."""
    result = runner.invoke(app, ["hello", "Alice"])
    assert result.exit_code == 0
    assert "Hello Alice!" in result.stdout

def test_hello_help():
    """Test de l'aide."""
    result = runner.invoke(app, ["hello", "--help"])
    assert result.exit_code == 0
    assert "Dit bonjour" in result.stdout


# === Test avec options ===
# test_app.py
from typer.testing import CliRunner
from app import app

runner = CliRunner()

def test_with_options():
    """Test avec options."""
    result = runner.invoke(app, ["run", "--verbose", "--config", "test.yml"])
    assert result.exit_code == 0
    assert "Verbose mode" in result.stdout


# === Test avec input ===
from typer.testing import CliRunner
from app import app

runner = CliRunner()

def test_with_input():
    """Test avec entrée utilisateur."""
    result = runner.invoke(app, ["login"], input="alice\npassword123\n")
    assert result.exit_code == 0
    assert "Logging in as alice" in result.stdout


# === Test avec fichiers temporaires ===
import tempfile
from pathlib import Path
from typer.testing import CliRunner
from app import app

runner = CliRunner()

def test_with_file():
    """Test avec fichier temporaire."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
        tmp.write("test content")
        tmp_path = tmp.name
    
    try:
        result = runner.invoke(app, ["process", tmp_path])
        assert result.exit_code == 0
    finally:
        Path(tmp_path).unlink()


# === Test avec isolated filesystem ===
from typer.testing import CliRunner
from app import app

runner = CliRunner()

def test_isolated_filesystem():
    """Test avec système de fichiers isolé."""
    with runner.isolated_filesystem():
        # Créer des fichiers de test
        Path("test.txt").write_text("test data")
        
        # Tester la commande
        result = runner.invoke(app, ["process", "test.txt"])
        assert result.exit_code == 0


# === Test avec pytest ===
# test_app.py
import pytest
from typer.testing import CliRunner
from app import app

@pytest.fixture
def runner():
    """Fixture pour le runner."""
    return CliRunner()

def test_create_user(runner):
    """Test création utilisateur."""
    result = runner.invoke(app, ["user", "create", "alice"])
    assert result.exit_code == 0
    assert "Created user: alice" in result.stdout

def test_invalid_command(runner):
    """Test commande invalide."""
    result = runner.invoke(app, ["invalid"])
    assert result.exit_code != 0


[OK] CONFIGURATION AVANCÉE

# === Application avec métadonnées ===
import typer

app = typer.Typer(
    name="myapp",
    help="Mon application CLI géniale",
    epilog="Créé avec [HEAVY_BLACK_HEART] par Mon Nom",
    add_completion=True,
    no_args_is_help=True,
    rich_markup_mode="rich"  # Support Rich markup
)

@app.command()
def hello():
    """Dit bonjour."""
    typer.echo("Hello!")

if __name__ == "__main__":
    app()


# === Commande cachée ===
import typer

app = typer.Typer()

@app.command()
def public():
    """Commande visible."""
    typer.echo("Public command")

@app.command(hidden=True)
def secret():
    """Commande cachée (n'apparaît pas dans --help)."""
    typer.echo("Secret command")

if __name__ == "__main__":
    app()


# === Commande dépréciée ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.command(deprecated=True)
def old_command():
    """Ancienne commande (dépréciée)."""
    typer.secho("Warning: This command is deprecated", fg=typer.colors.YELLOW)
    typer.echo("Please use 'new_command' instead")

@app.command()
def new_command():
    """Nouvelle commande recommandée."""
    typer.echo("New command")

if __name__ == "__main__":
    app()


# === Callback d'aide personnalisé ===
import typer
from typing_extensions import Annotated

def version_callback(value: bool):
    """Affiche la version."""
    if value:
        typer.echo("My App v1.0.0")
        raise typer.Exit()

app = typer.Typer()

@app.callback()
def main(
    version: Annotated[bool, typer.Option(
        "--version",
        "-v",
        callback=version_callback,
        is_eager=True,
        help="Show version"
    )] = False
):
    """Mon CLI."""
    pass

@app.command()
def run():
    """Lance l'app."""
    typer.echo("Running...")

if __name__ == "__main__":
    app()


# === Désactiver l'aide ===
import typer

app = typer.Typer(add_help_option=False)

@app.command()
def run():
    """Lance l'app."""
    typer.echo("Running...")

if __name__ == "__main__":
    app()


# === Personnaliser le nom de l'aide ===
import typer

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    help: bool = typer.Option(False, "--aide", "-a", help="Afficher l'aide")
):
    """Mon CLI en français."""
    if help:
        typer.echo(ctx.get_help())
        raise typer.Exit()

@app.command()
def lancer():
    """Lance l'application."""
    typer.echo("Lancement...")

if __name__ == "__main__":
    app()


[OK] INTÉGRATION AVEC D'AUTRES BIBLIOTHÈQUES

# === Avec Rich (affichage amélioré) ===
import typer
from rich.console import Console
from rich.table import Table
from rich.progress import track
import time

console = Console()

app = typer.Typer()

@app.command()
def show_table():
    """Affiche un tableau avec Rich."""
    table = Table(title="Users")
    table.add_column("Name", style="cyan")
    table.add_column("Age", style="magenta")
    table.add_column("Email", style="green")
    
    table.add_row("Alice", "25", "alice@example.com")
    table.add_row("Bob", "30", "bob@example.com")
    table.add_row("Charlie", "35", "charlie@example.com")
    
    console.print(table)

@app.command()
def process():
    """Traite avec progress bar Rich."""
    for _ in track(range(100), description="Processing..."):
        time.sleep(0.01)
    console.print("[green][OK] Done!")

if __name__ == "__main__":
    app()


# === Avec Pydantic (validation) ===
import typer
from pydantic import BaseModel, EmailStr, Field, ValidationError

class User(BaseModel):
    """Modèle utilisateur."""
    username: str = Field(min_length=3, max_length=20)
    email: EmailStr
    age: int = Field(ge=18, le=120)

app = typer.Typer()

@app.command()
def create_user(username: str, email: str, age: int):
    """Crée un utilisateur avec validation Pydantic."""
    try:
        user = User(username=username, email=email, age=age)
        typer.secho(f"[OK] User created: {user.username}", fg=typer.colors.GREEN)
    except ValidationError as e:
        typer.secho("Validation errors:", fg=typer.colors.RED, err=True)
        for error in e.errors():
            typer.echo(f"  - {error['loc'][0]}: {error['msg']}", err=True)
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()


# === Avec Requests (API) ===
import typer
import requests
from typing_extensions import Annotated

app = typer.Typer()

@app.command()
def get_user(
    user_id: int,
    api_url: Annotated[str, typer.Option()] = "https://api.example.com"
):
    """Récupère un utilisateur depuis l'API."""
    with typer.progressbar(length=1, label="Fetching user") as progress:
        try:
            response = requests.get(f"{api_url}/users/{user_id}")
            response.raise_for_status()
            progress.update(1)
        except requests.RequestException as e:
            typer.secho(f"Error: {e}", fg=typer.colors.RED, err=True)
            raise typer.Exit(code=1)
    
    user = response.json()
    typer.echo(f"User: {user['name']}")
    typer.echo(f"Email: {user['email']}")

if __name__ == "__main__":
    app()


# === Avec SQLAlchemy (base de données) ===
import typer
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    """Modèle utilisateur."""
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    email = Column(String(100))

app = typer.Typer()

@app.callback()
def setup(ctx: typer.Context):
    """Configure la base de données."""
    engine = create_engine('sqlite:///app.db')
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    ctx.obj = {'session': Session()}

@app.command()
def add_user(ctx: typer.Context, username: str, email: str):
    """Ajoute un utilisateur."""
    session = ctx.obj['session']
    user = User(username=username, email=email)
    session.add(user)
    session.commit()
    typer.secho(f"[OK] User {username} added", fg=typer.colors.GREEN)

if __name__ == "__main__":
    app()


# === Avec Click (interopérabilité) ===
import typer
import click

# Typer est compatible avec Click
app = typer.Typer()

@app.command()
@click.pass_context
def mixed_command(ctx: click.Context, name: str):
    """Commande mixte Typer/Click."""
    typer.echo(f"Hello {name} from mixed command!")

if __name__ == "__main__":
    app()


[OK] AUTO-COMPLÉTION SHELL

# === Installer l'auto-complétion ===

# Bash
# typer myapp.py utils docs --name myapp --output myapp-complete.sh
# source myapp-complete.sh

# Zsh
# typer myapp.py utils docs --name myapp --shell zsh --output myapp-complete.zsh
# source myapp-complete.zsh

# Fish
# typer myapp.py utils docs --name myapp --shell fish --output myapp-complete.fish
# source myapp-complete.fish

# PowerShell
# typer myapp.py utils docs --name myapp --shell powershell --output myapp-complete.ps1
# . myapp-complete.ps1


# === Activer auto-complétion dans l'app ===
import typer

app = typer.Typer(add_completion=True)

@app.command()
def hello(name: str):
    """Dit bonjour."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    app()

# python app.py --install-completion
# python app.py --show-completion


# === Auto-complétion personnalisée ===
import typer
from typing_extensions import Annotated

def complete_names():
    """Fonction de complétion personnalisée."""
    return ["Alice", "Bob", "Charlie"]

def hello(
    name: Annotated[str, typer.Argument(autocompletion=complete_names)]
):
    """Dit bonjour avec auto-complétion."""
    typer.echo(f"Hello {name}!")

if __name__ == "__main__":
    typer.run(hello)


# === Complétion dynamique ===
import typer
from pathlib import Path
from typing_extensions import Annotated

def complete_python_files(incomplete: str):
    """Complète avec les fichiers Python."""
    completion = []
    for path in Path(".").glob(f"{incomplete}*.py"):
        completion.append(path.name)
    return completion

def process(
    file: Annotated[str, typer.Argument(autocompletion=complete_python_files)]
):
    """Traite un fichier Python."""
    typer.echo(f"Processing {file}")

if __name__ == "__main__":
    typer.run(process)


[OK] EXEMPLES COMPLETS D'APPLICATIONS

# === CLI pour gestion de tâches (TODO) ===
# todo.py
import typer
from pathlib import Path
import json
from typing import List, Optional
from typing_extensions import Annotated
from rich.console import Console
from rich.table import Table

app = typer.Typer(help="Gestionnaire de tâches simple")
console = Console()

TODO_FILE = Path("todos.json")

def load_todos() -> List[dict]:
    """Charge les tâches."""
    if not TODO_FILE.exists():
        return []
    return json.loads(TODO_FILE.read_text())

def save_todos(todos: List[dict]):
    """Sauvegarde les tâches."""
    TODO_FILE.write_text(json.dumps(todos, indent=2))

@app.command()
def add(task: str, priority: Annotated[int, typer.Option(min=1, max=5)] = 3):
    """Ajoute une nouvelle tâche."""
    todos = load_todos()
    todo = {
        "id": len(todos) + 1,
        "task": task,
        "priority": priority,
        "done": False
    }
    todos.append(todo)
    save_todos(todos)
    console.print(f"[green][OK] Task added: {task}[/green]")

@app.command("list")
def list_todos(
    all: Annotated[bool, typer.Option("--all", "-a")] = False
):
    """Liste les tâches."""
    todos = load_todos()
    
    if not all:
        todos = [t for t in todos if not t["done"]]
    
    if not todos:
        console.print("[yellow]No tasks found[/yellow]")
        return
    
    table = Table(title="Tasks")
    table.add_column("ID", style="cyan")
    table.add_column("Task", style="white")
    table.add_column("Priority", style="magenta")
    table.add_column("Status", style="green")
    
    for todo in todos:
        status = "[OK]" if todo["done"] else "[WHITE_CIRCLE]"
        table.add_row(
            str(todo["id"]),
            todo["task"],
            str(todo["priority"]),
            status
        )
    
    console.print(table)

@app.command()
def done(task_id: int):
    """Marque une tâche comme terminée."""
    todos = load_todos()
    
    for todo in todos:
        if todo["id"] == task_id:
            todo["done"] = True
            save_todos(todos)
            console.print(f"[green][OK] Task {task_id} marked as done[/green]")
            return
    
    console.print(f"[red]Task {task_id} not found[/red]")
    raise typer.Exit(code=1)

@app.command()
def delete(task_id: int):
    """Supprime une tâche."""
    todos = load_todos()
    todos = [t for t in todos if t["id"] != task_id]
    save_todos(todos)
    console.print(f"[green][OK] Task {task_id} deleted[/green]")

@app.command()
def clear():
    """Supprime toutes les tâches terminées."""
    if typer.confirm("Delete all completed tasks?"):
        todos = load_todos()
        todos = [t for t in todos if not t["done"]]
        save_todos(todos)
        console.print("[green][OK] Completed tasks deleted[/green]")
    else:
        console.print("Cancelled")

if __name__ == "__main__":
    app()

# Utilisation:
# python todo.py add "Acheter du lait" --priority 5
# python todo.py list
# python todo.py done 1
# python todo.py delete 2
# python todo.py clear


# === CLI pour gestion de fichiers ===
# filemanager.py
import typer
from pathlib import Path
from typing_extensions import Annotated
from rich.console import Console
from rich.tree import Tree
import shutil

app = typer.Typer(help="Gestionnaire de fichiers CLI")
console = Console()

@app.command()
def ls(
    directory: Annotated[Path, typer.Argument()] = Path("."),
    all: Annotated[bool, typer.Option("--all", "-a")] = False
):
    """Liste les fichiers d'un dossier."""
    if not directory.exists():
        console.print(f"[red]Error: {directory} not found[/red]")
        raise typer.Exit(code=1)
    
    for item in sorted(directory.iterdir()):
        if not all and item.name.startswith('.'):
            continue
        
        if item.is_file():
            size = item.stat().st_size
            console.print(f"[FICHIER] {item.name} [dim]({size} bytes)[/dim]")
        else:
            console.print(f"[DOSSIER] {item.name}/")

@app.command()
def tree(
    directory: Annotated[Path, typer.Argument()] = Path("."),
    depth: Annotated[int, typer.Option()] = 2
):
    """Affiche l'arborescence des fichiers."""
    def build_tree(path: Path, tree: Tree, current_depth: int = 0):
        if current_depth >= depth:
            return
        
        try:
            for item in sorted(path.iterdir()):
                if item.is_file():
                    tree.add(f"[FICHIER] {item.name}")
                else:
                    branch = tree.add(f"[DOSSIER] {item.name}/")
                    build_tree(item, branch, current_depth + 1)
        except PermissionError:
            tree.add("[red]Permission denied[/red]")
    
    root_tree = Tree(f"[DOSSIER] {directory.name}/")
    build_tree(directory, root_tree)
    console.print(root_tree)

@app.command()
def cp(source: Path, dest: Path, recursive: Annotated[bool, typer.Option("-r")] = False):
    """Copie un fichier ou dossier."""
    if not source.exists():
        console.print(f"[red]Error: {source} not found[/red]")
        raise typer.Exit(code=1)
    
    try:
        if source.is_file():
            shutil.copy2(source, dest)
        elif recursive:
            shutil.copytree(source, dest)
        else:
            console.print("[red]Error: Use -r to copy directories[/red]")
            raise typer.Exit(code=1)
        
        console.print(f"[green][OK] Copied {source} to {dest}[/green]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

@app.command()
def mv(source: Path, dest: Path):
    """Déplace un fichier ou dossier."""
    if not source.exists():
        console.print(f"[red]Error: {source} not found[/red]")
        raise typer.Exit(code=1)
    
    shutil.move(str(source), str(dest))
    console.print(f"[green][OK] Moved {source} to {dest}[/green]")

@app.command()
def rm(
    path: Path,
    recursive: Annotated[bool, typer.Option("-r")] = False,
    force: Annotated[bool, typer.Option("-f")] = False
):
    """Supprime un fichier ou dossier."""
    if not path.exists():
        console.print(f"[red]Error: {path} not found[/red]")
        raise typer.Exit(code=1)
    
    if not force:
        if not typer.confirm(f"Delete {path}?"):
            console.print("Cancelled")
            return
    
    try:
        if path.is_file():
            path.unlink()
        elif recursive:
            shutil.rmtree(path)
        else:
            console.print("[red]Error: Use -r to delete directories[/red]")
            raise typer.Exit(code=1)
        
        console.print(f"[green][OK] Deleted {path}[/green]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

@app.command()
def mkdir(path: Path, parents: Annotated[bool, typer.Option("-p")] = False):
    """Crée un dossier."""
    try:
        path.mkdir(parents=parents, exist_ok=False)
        console.print(f"[green][OK] Created directory {path}[/green]")
    except FileExistsError:
        console.print(f"[red]Error: {path} already exists[/red]")
        raise typer.Exit(code=1)
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()


# === CLI pour API REST ===
# api_client.py
import typer
import requests
from typing import Optional
from typing_extensions import Annotated
from rich.console import Console
from rich.json import JSON

app = typer.Typer(help="Client API REST")
console = Console()

BASE_URL = "https://jsonplaceholder.typicode.com"

@app.callback()
def main(
    ctx: typer.Context,
    base_url: Annotated[str, typer.Option(envvar="API_BASE_URL")] = BASE_URL
):
    """Configure l'URL de base de l'API."""
    ctx.obj = {"base_url": base_url}

@app.command()
def get(
    ctx: typer.Context,
    endpoint: str,
    id: Annotated[Optional[int], typer.Option()] = None
):
    """Effectue une requête GET."""
    url = f"{ctx.obj['base_url']}/{endpoint}"
    if id:
        url += f"/{id}"
    
    try:
        with console.status("[bold green]Fetching data..."):
            response = requests.get(url)
            response.raise_for_status()
        
        data = response.json()
        console.print(JSON.from_data(data))
    except requests.RequestException as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

@app.command()
def post(
    ctx: typer.Context,
    endpoint: str,
    data: str
):
    """Effectue une requête POST."""
    import json
    
    url = f"{ctx.obj['base_url']}/{endpoint}"
    
    try:
        payload = json.loads(data)
        response = requests.post(url, json=payload)
        response.raise_for_status()
        
        console.print("[green][OK] Created successfully[/green]")
        console.print(JSON.from_data(response.json()))
    except json.JSONDecodeError:
        console.print("[red]Error: Invalid JSON data[/red]")
        raise typer.Exit(code=1)
    except requests.RequestException as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

@app.command()
def delete(ctx: typer.Context, endpoint: str, id: int):
    """Effectue une requête DELETE."""
    url = f"{ctx.obj['base_url']}/{endpoint}/{id}"
    
    if not typer.confirm(f"Delete {endpoint}/{id}?"):
        console.print("Cancelled")
        return
    
    try:
        response = requests.delete(url)
        response.raise_for_status()
        console.print(f"[green][OK] Deleted {endpoint}/{id}[/green]")
    except requests.RequestException as e:
        console.print(f"[red]Error: {e}[/red]")
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()

# Utilisation:
# python api_client.py get posts --id 1
# python api_client.py post posts '{"title":"Test","body":"Content","userId":1}'
# python api_client.py delete posts 1


# === CLI pour base de données ===
# db_cli.py
import typer
from pathlib import Path
from typing_extensions import Annotated
from rich.console import Console
from rich.table import Table
import sqlite3

app = typer.Typer(help="Gestionnaire de base de données SQLite")
console = Console()

@app.callback()
def main(
    ctx: typer.Context,
    db: Annotated[Path, typer.Option()] = Path("app.db")
):
    """Configure la base de données."""
    ctx.obj = {"db": db}

@app.command()
def init(ctx: typer.Context):
    """Initialise la base de données."""
    db_path = ctx.obj["db"]
    
    if db_path.exists():
        if not typer.confirm(f"{db_path} already exists. Overwrite?"):
            console.print("Cancelled")
            return
        db_path.unlink()
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    cursor.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            username TEXT NOT NULL UNIQUE,
            email TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    
    conn.commit()
    conn.close()
    
    console.print(f"[green][OK] Database initialized: {db_path}[/green]")

@app.command()
def add_user(
    ctx: typer.Context,
    username: str,
    email: str
):
    """Ajoute un utilisateur."""
    db_path = ctx.obj["db"]
    
    if not db_path.exists():
        console.print("[red]Error: Database not initialized. Run 'init' first.[/red]")
        raise typer.Exit(code=1)
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    try:
        cursor.execute(
            "INSERT INTO users (username, email) VALUES (?, ?)",
            (username, email)
        )
        conn.commit()
        console.print(f"[green][OK] User {username} added[/green]")
    except sqlite3.IntegrityError:
        console.print(f"[red]Error: Username {username} already exists[/red]")
        raise typer.Exit(code=1)
    finally:
        conn.close()

@app.command()
def list_users(ctx: typer.Context):
    """Liste tous les utilisateurs."""
    db_path = ctx.obj["db"]
    
    if not db_path.exists():
        console.print("[red]Error: Database not initialized[/red]")
        raise typer.Exit(code=1)
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    cursor.execute("SELECT id, username, email, created_at FROM users")
    users = cursor.fetchall()
    conn.close()
    
    if not users:
        console.print("[yellow]No users found[/yellow]")
        return
    
    table = Table(title="Users")
    table.add_column("ID", style="cyan")
    table.add_column("Username", style="green")
    table.add_column("Email", style="blue")
    table.add_column("Created", style="magenta")
    
    for user in users:
        table.add_row(str(user[0]), user[1], user[2], user[3])
    
    console.print(table)

@app.command()
def delete_user(ctx: typer.Context, user_id: int):
    """Supprime un utilisateur."""
    db_path = ctx.obj["db"]
    
    if not db_path.exists():
        console.print("[red]Error: Database not initialized[/red]")
        raise typer.Exit(code=1)
    
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    
    cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
    
    if cursor.rowcount == 0:
        console.print(f"[red]Error: User {user_id} not found[/red]")
        raise typer.Exit(code=1)
    
    conn.commit()
    conn.close()
    
    console.print(f"[green][OK] User {user_id} deleted[/green]")

if __name__ == "__main__":
    app()


# === CLI avec configuration YAML ===
# config_app.py
import typer
from pathlib import Path
from typing_extensions import Annotated
from rich.console import Console
import yaml

app = typer.Typer(help="Application avec configuration YAML")
console = Console()

CONFIG_FILE = Path("config.yml")

def load_config() -> dict:
    """Charge la configuration."""
    if not CONFIG_FILE.exists():
        return {}
    with CONFIG_FILE.open() as f:
        return yaml.safe_load(f) or {}

def save_config(config: dict):
    """Sauvegarde la configuration."""
    with CONFIG_FILE.open('w') as f:
        yaml.dump(config, f, default_flow_style=False)

@app.command()
def init():
    """Initialise le fichier de configuration."""
    if CONFIG_FILE.exists():
        if not typer.confirm("Config file exists. Overwrite?"):
            console.print("Cancelled")
            return
    
    default_config = {
        "app": {
            "name": "My App",
            "version": "1.0.0"
        },
        "database": {
            "host": "localhost",
            "port": 5432,
            "name": "mydb"
        },
        "logging": {
            "level": "INFO",
            "file": "app.log"
        }
    }
    
    save_config(default_config)
    console.print(f"[green][OK] Config file created: {CONFIG_FILE}[/green]")

@app.command()
def show():
    """Affiche la configuration."""
    config = load_config()
    
    if not config:
        console.print("[yellow]No configuration found. Run 'init' first.[/yellow]")
        return
    
    console.print(yaml.dump(config, default_flow_style=False))

@app.command()
def get(key: str):
    """Récupère une valeur de configuration."""
    config = load_config()
    
    keys = key.split('.')
    value = config
    
    try:
        for k in keys:
            value = value[k]
        console.print(f"{key} = {value}")
    except KeyError:
        console.print(f"[red]Error: Key '{key}' not found[/red]")
        raise typer.Exit(code=1)

@app.command()
def set(key: str, value: str):
    """Définit une valeur de configuration."""
    config = load_config()
    
    keys = key.split('.')
    current = config
    
    for k in keys[:-1]:
        if k not in current:
            current[k] = {}
        current = current[k]
    
    # Tenter de convertir en int/float/bool
    try:
        if value.lower() in ['true', 'false']:
            value = value.lower() == 'true'
        elif value.isdigit():
            value = int(value)
        elif '.' in value:
            value = float(value)
    except:
        pass
    
    current[keys[-1]] = value
    save_config(config)
    console.print(f"[green][OK] {key} = {value}[/green]")

if __name__ == "__main__":
    app()


[OK] BONNES PRATIQUES

# 1. Structure de projet
"""
myproject/
├── myproject/
│   ├── __init__.py
│   ├── cli.py              # Point d'entrée CLI
│   ├── commands/           # Commandes séparées
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── project.py
│   ├── utils.py            # Utilitaires
│   └── config.py           # Configuration
├── tests/
│   ├── __init__.py
│   └── test_cli.py
├── setup.py
├── pyproject.toml
└── README.md
"""

# 2. Point d'entrée dans setup.py
"""
from setuptools import setup

setup(
    name="myproject",
    version="1.0.0",
    py_modules=["myproject"],
    install_requires=[
        "typer[all]>=0.9.0",
    ],
    entry_points={
        "console_scripts": [
            "myapp=myproject.cli:app",
        ],
    },
)
"""

# 3. Séparer les commandes
# commands/user.py
import typer

app = typer.Typer()

@app.command()
def create(username: str):
    """Crée un utilisateur."""
    typer.echo(f"Creating user: {username}")

@app.command()
def delete(username: str):
    """Supprime un utilisateur."""
    typer.echo(f"Deleting user: {username}")


# cli.py
import typer
from myproject.commands import user, project

app = typer.Typer()
app.add_typer(user.app, name="user")
app.add_typer(project.app, name="project")

if __name__ == "__main__":
    app()


# 4. Utiliser les type hints
from typing_extensions import Annotated

def command(
    name: Annotated[str, typer.Argument(help="User name")],
    age: Annotated[int, typer.Option(min=0, max=120)],
    email: Annotated[str, typer.Option()]
):
    """Commande bien typée."""
    pass


# 5. Valider les entrées
def validate_email(email: str) -> str:
    """Valide une adresse email."""
    if "@" not in email or "." not in email:
        raise typer.BadParameter("Invalid email format")
    return email


# 6. Gérer les erreurs proprement
@app.command()
def safe_command():
    """Commande avec gestion d'erreur."""
    try:
        # Code risqué
        result = risky_operation()
        typer.secho("[OK] Success", fg=typer.colors.GREEN)
    except ValueError as e:
        typer.secho(f"Error: {e}", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=1)
    except Exception as e:
        typer.secho(f"Unexpected error: {e}", fg=typer.colors.RED, err=True)
        raise typer.Exit(code=2)


# 7. Utiliser le contexte pour l'état partagé
@app.callback()
def setup(
    ctx: typer.Context,
    verbose: bool = False
):
    """Configure l'état global."""
    ctx.ensure_object(dict)
    ctx.obj['verbose'] = verbose
    ctx.obj['config'] = load_config()


# 8. Documenter avec docstrings
@app.command()
def well_documented(name: str, age: int = 18):
    """
    Commande bien documentée.
    
    Cette commande fait quelque chose d'important et est bien documentée
    pour que les utilisateurs comprennent ce qu'elle fait.
    
    Args:
        name: Le nom de la personne
        age: L'âge de la personne (par défaut: 18)
    
    Examples:
        $ myapp well-documented "Alice" --age 25
    """
    typer.echo(f"{name} is {age} years old")


# 9. Tester le CLI
from typer.testing import CliRunner

def test_command():
    """Test de commande."""
    runner = CliRunner()
    result = runner.invoke(app, ["command", "arg"])
    assert result.exit_code == 0
    assert "expected output" in result.stdout


# 10. Utiliser Rich pour un meilleur affichage
from rich.console import Console
from rich.progress import track

console = Console()

@app.command()
def pretty_command():
    """Commande avec affichage Rich."""
    console.print("[bold green]Starting process...[/bold green]")
    
    for i in track(range(100), description="Processing"):
        # Traitement
        pass
    
    console.print("[green][OK] Done![/green]")


[OK] DÉPANNAGE

# === Problème: ModuleNotFoundError ===
# Solution: Installer typer
pip install "typer[all]"

# === Problème: Type hints ne fonctionnent pas ===
# Solution: Importer depuis typing_extensions
from typing_extensions import Annotated

# Pour Python 3.9+, vous pouvez aussi utiliser:
from typing import Annotated

# === Problème: Auto-complétion ne marche pas ===
# Solution: Installer avec [all]
pip install "typer[all]"

# Puis installer la complétion
python app.py --install-completion

# === Problème: Couleurs ne s'affichent pas ===
# Solution: Forcer les couleurs
import typer
typer.echo("Text", color=True)

# Ou vérifier le support des couleurs
if typer.style_support():
    typer.secho("Colored", fg=typer.colors.GREEN)

# === Problème: Progress bar ne s'affiche pas ===
# Solution: Utiliser file=sys.stderr
import sys
with typer.progressbar(items, file=sys.stderr) as progress:
    for item in progress:
        process(item)

# === Problème: Prompt ne fonctionne pas en test ===
# Solution: Utiliser input dans CliRunner
from typer.testing import CliRunner

runner = CliRunner()
result = runner.invoke(app, ["login"], input="alice\npassword\n")

# === Problème: Path validation échoue ===
# Solution: Vérifier les options de Path
from pathlib import Path
from typing_extensions import Annotated

def command(
    file: Annotated[Path, typer.Argument(
        exists=True,           # Vérifie l'existence
        file_okay=True,        # Accepte les fichiers
        dir_okay=False,        # Refuse les dossiers
        readable=True,         # Vérifie la lecture
        resolve_path=True      # Résout le chemin complet
    )]
):
    pass

# === Problème: Commande non reconnue ===
# Solution: Vérifier l'enregistrement
app = typer.Typer()

@app.command()  # Ne pas oublier le décorateur!
def mycommand():
    pass

# === Problème: Options globales ne fonctionnent pas ===
# Solution: Utiliser @app.callback()
@app.callback()
def main(verbose: bool = False):
    """Options globales."""
    global VERBOSE
    VERBOSE = verbose


[OK] RESSOURCES

# Documentation officielle
# https://typer.tiangolo.com/

# Tutoriels
# https://typer.tiangolo.com/tutorial/

# GitHub
# https://github.com/tiangolo/typer

# Exemples
# https://typer.tiangolo.com/tutorial/commands/

# Rich (pour affichage amélioré)
# https://rich.readthedocs.io/

# Click (base de Typer)
# https://click.palletsprojects.com/

# Alternatives à Typer
# - Click: Bas niveau, plus de contrôle
# - argparse: Standard library
# - fire: Google, très automatique
# - cliff: OpenStack, pour CLI complexes


[OK] COMPARAISON AVEC D'AUTRES OUTILS

# === Typer vs argparse ===

# argparse (stdlib)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('name')
parser.add_argument('--age', type=int, default=18)
args = parser.parse_args()
print(f"Hello {args.name}, age {args.age}")

# Typer (moderne)
import typer
def greet(name: str, age: int = 18):
    typer.echo(f"Hello {name}, age {age}")
typer.run(greet)


# === Typer vs Click ===

# Click
import click
@click.command()
@click.argument('name')
@click.option('--age', default=18)
def greet(name, age):
    click.echo(f"Hello {name}, age {age}")

# Typer
import typer
def greet(name: str, age: int = 18):
    typer.echo(f"Hello {name}, age {age}")
typer.run(greet)


# === Typer vs Fire ===

# Fire
import fire
def greet(name, age=18):
    print(f"Hello {name}, age {age}")
fire.Fire(greet)

# Typer
import typer
def greet(name: str, age: int = 18):
    typer.echo(f"Hello {name}, age {age}")
typer.run(greet)


[OK] ASTUCES AVANCÉES

# === Lazy loading pour performances ===
import typer

app = typer.Typer()

@app.command()
def heavy_command():
    """Commande qui charge des modules lourds."""
    import pandas  # Import seulement quand nécessaire
    import numpy
    # Code de la commande
    typer.echo("Processing with pandas and numpy")

if __name__ == "__main__":
    app()


# === Utiliser des alias pour les commandes ===
import typer

app = typer.Typer()

@app.command("ls")
@app.command("list")  # Alias
def list_items():
    """Liste les éléments (ls ou list)."""
    typer.echo("Listing items")

if __name__ == "__main__":
    app()


# === Commandes conditionnelles ===
import typer
import sys

app = typer.Typer()

@app.command()
def admin_command():
    """Commande admin uniquement."""
    if not is_admin():
        typer.secho("Error: Admin privileges required", fg=typer.colors.RED)
        raise typer.Exit(code=1)
    typer.echo("Admin command executed")

def is_admin():
    # Vérifier si l'utilisateur est admin
    return False

if __name__ == "__main__":
    app()


# === Logging intégré ===
import typer
import logging
from typing_extensions import Annotated

app = typer.Typer()

@app.callback()
def setup_logging(
    verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False
):
    """Configure le logging."""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(levelname)s - %(message)s'
    )

@app.command()
def run():
    """Lance l'application avec logging."""
    logging.info("Starting application")
    logging.debug("Debug information")
    typer.echo("Running application")

if __name__ == "__main__":
    app()


# === Spinner personnalisé avec Rich ===
import typer
from rich.console import Console
from rich.spinner import Spinner
import time

console = Console()

def long_operation():
    """Opération longue avec spinner."""
    with console.status("[bold green]Processing...", spinner="dots") as status:
        time.sleep(2)
        status.update("[bold yellow]Almost done...")
        time.sleep(2)
    
    console.print("[green][OK] Complete![/green]")

if __name__ == "__main__":
    typer.run(long_operation)


# === Variables d'environnement avec .env ===
import typer
from pathlib import Path
from typing_extensions import Annotated
from dotenv import load_dotenv
import os

# Charger .env
load_dotenv()

app = typer.Typer()

@app.command()
def connect(
    host: Annotated[str, typer.Option(envvar="DB_HOST")] = "localhost",
    port: Annotated[int, typer.Option(envvar="DB_PORT")] = 5432,
    user: Annotated[str, typer.Option(envvar="DB_USER")] = "postgres"
):
    """Se connecte à la base de données."""
    typer.echo(f"Connecting to {user}@{host}:{port}")

if __name__ == "__main__":
    app()

# Fichier .env:
# DB_HOST=production.db.com
# DB_PORT=3306
# DB_USER=admin


# === Middleware pattern ===
import typer
from functools import wraps
import time

def timing_middleware(func):
    """Mesure le temps d'exécution."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        typer.secho(f"[TEMPS]  Executed in {elapsed:.2f}s", fg=typer.colors.BLUE)
        return result
    return wrapper

app = typer.Typer()

@app.command()
@timing_middleware
def slow_command():
    """Commande lente."""
    time.sleep(2)
    typer.echo("Done!")

if __name__ == "__main__":
    app()


# === Plugins system ===
import typer
from pathlib import Path
import importlib.util

app = typer.Typer()

def load_plugins():
    """Charge les plugins depuis le dossier plugins/."""
    plugins_dir = Path("plugins")
    if not plugins_dir.exists():
        return
    
    for plugin_file in plugins_dir.glob("*.py"):
        if plugin_file.name.startswith("_"):
            continue
        
        # Charger le module
        spec = importlib.util.spec_from_file_location(
            plugin_file.stem, plugin_file
        )
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        
        # Ajouter les commandes du plugin
        if hasattr(module, "plugin_app"):
            app.add_typer(module.plugin_app, name=plugin_file.stem)

# Charger les plugins au démarrage
load_plugins()

@app.command()
def main_command():
    """Commande principale."""
    typer.echo("Main command")

if __name__ == "__main__":
    app()

# Fichier plugins/my_plugin.py:
"""
import typer

plugin_app = typer.Typer()

@plugin_app.command()
def plugin_command():
    typer.echo("Plugin command!")
"""


# === Internationalisation (i18n) ===
import typer
from typing_extensions import Annotated

TRANSLATIONS = {
    "en": {
        "hello": "Hello",
        "goodbye": "Goodbye"
    },
    "fr": {
        "hello": "Bonjour",
        "goodbye": "Au revoir"
    },
    "es": {
        "hello": "Hola",
        "goodbye": "Adiós"
    }
}

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    lang: Annotated[str, typer.Option(envvar="LANG")] = "en"
):
    """Configure la langue."""
    ctx.ensure_object(dict)
    ctx.obj["lang"] = lang if lang in TRANSLATIONS else "en"

@app.command()
def greet(ctx: typer.Context, name: str):
    """Salue quelqu'un."""
    lang = ctx.obj["lang"]
    message = TRANSLATIONS[lang]["hello"]
    typer.echo(f"{message} {name}!")

if __name__ == "__main__":
    app()

# Utilisation:
# export LANG=fr
# python app.py greet Alice
# Output: Bonjour Alice!


# === Rate limiting ===
import typer
from functools import wraps
import time

# Dictionnaire pour suivre les appels
_last_call = {}

def rate_limit(seconds: int):
    """Limite le taux d'appel d'une commande."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            func_name = func.__name__
            
            if func_name in _last_call:
                elapsed = now - _last_call[func_name]
                if elapsed < seconds:
                    remaining = seconds - elapsed
                    typer.secho(
                        f"Please wait {remaining:.1f}s before calling again",
                        fg=typer.colors.YELLOW
                    )
                    raise typer.Exit(code=1)
            
            _last_call[func_name] = now
            return func(*args, **kwargs)
        return wrapper
    return decorator

app = typer.Typer()

@app.command()
@rate_limit(seconds=5)
def api_call():
    """Appel API avec rate limiting."""
    typer.echo("Making API call...")

if __name__ == "__main__":
    app()


# === Configuration hiérarchique ===
import typer
from pathlib import Path
from typing import Optional
from typing_extensions import Annotated
import json

def load_config_hierarchy() -> dict:
    """
    Charge la configuration depuis plusieurs sources (priorité décroissante):
    1. Variables d'environnement
    2. ~/.myapp/config.json (utilisateur)
    3. /etc/myapp/config.json (système)
    4. config.json (local)
    """
    config = {}
    
    # Config par défaut
    config.update({
        "host": "localhost",
        "port": 8000,
        "debug": False
    })
    
    # Config locale
    local_config = Path("config.json")
    if local_config.exists():
        config.update(json.loads(local_config.read_text()))
    
    # Config système
    system_config = Path("/etc/myapp/config.json")
    if system_config.exists():
        config.update(json.loads(system_config.read_text()))
    
    # Config utilisateur
    user_config = Path.home() / ".myapp" / "config.json"
    if user_config.exists():
        config.update(json.loads(user_config.read_text()))
    
    # Variables d'environnement (priorité max)
    import os
    if "MYAPP_HOST" in os.environ:
        config["host"] = os.environ["MYAPP_HOST"]
    if "MYAPP_PORT" in os.environ:
        config["port"] = int(os.environ["MYAPP_PORT"])
    
    return config

app = typer.Typer()

@app.callback()
def setup(ctx: typer.Context):
    """Charge la configuration."""
    ctx.obj = load_config_hierarchy()

@app.command()
def show_config(ctx: typer.Context):
    """Affiche la configuration."""
    config = ctx.obj
    for key, value in config.items():
        typer.echo(f"{key}: {value}")

if __name__ == "__main__":
    app()


# === Sous-commandes dynamiques ===
import typer
from typing import List

app = typer.Typer()

def create_command(name: str, description: str):
    """Crée dynamiquement une commande."""
    def command_func():
        typer.echo(f"Executing {name}: {description}")
    
    command_func.__name__ = name
    command_func.__doc__ = description
    return command_func

# Créer des commandes dynamiquement
commands_config = [
    {"name": "backup", "description": "Backup database"},
    {"name": "restore", "description": "Restore database"},
    {"name": "migrate", "description": "Run migrations"}
]

for cmd_config in commands_config:
    command = create_command(cmd_config["name"], cmd_config["description"])
    app.command()(command)

if __name__ == "__main__":
    app()


# === Gestion des signaux (Ctrl+C) ===
import typer
import signal
import sys

app = typer.Typer()

def signal_handler(sig, frame):
    """Gère Ctrl+C proprement."""
    typer.echo("\n")
    typer.secho("Interrupted by user", fg=typer.colors.YELLOW)
    typer.echo("Cleaning up...")
    # Code de nettoyage ici
    sys.exit(0)

@app.command()
def long_running():
    """Tâche longue interruptible."""
    # Configurer le handler
    signal.signal(signal.SIGINT, signal_handler)
    
    typer.echo("Running... (Press Ctrl+C to stop)")
    import time
    while True:
        typer.echo("Working...")
        time.sleep(1)

if __name__ == "__main__":
    app()


# === Dry-run mode ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    dry_run: Annotated[bool, typer.Option("--dry-run")] = False
):
    """Configure le mode dry-run."""
    ctx.ensure_object(dict)
    ctx.obj["dry_run"] = dry_run
    if dry_run:
        typer.secho("DRY RUN MODE - No changes will be made", fg=typer.colors.YELLOW)

@app.command()
def delete(ctx: typer.Context, file: str):
    """Supprime un fichier."""
    if ctx.obj["dry_run"]:
        typer.echo(f"Would delete: {file}")
    else:
        typer.echo(f"Deleting: {file}")
        # Code de suppression réel

if __name__ == "__main__":
    app()


# === Commandes interactives ===
import typer
from typing_extensions import Annotated

app = typer.Typer()

@app.command()
def interactive():
    """Mode interactif."""
    typer.secho("=== Interactive Mode ===", bold=True)
    typer.echo("Type 'help' for commands, 'exit' to quit")
    
    while True:
        try:
            command = typer.prompt("\n>", prompt_suffix=" ")
            
            if command == "exit":
                typer.echo("Goodbye!")
                break
            elif command == "help":
                typer.echo("Available commands: help, exit, status")
            elif command == "status":
                typer.echo("Status: Running")
            else:
                typer.echo(f"Unknown command: {command}")
        except (KeyboardInterrupt, EOFError):
            typer.echo("\nGoodbye!")
            break

if __name__ == "__main__":
    app()


# === Pipeline de commandes ===
import typer
from typing import Optional
from typing_extensions import Annotated

app = typer.Typer()

@app.command()
def process(
    input_file: str,
    output_file: Optional[str] = None,
    uppercase: Annotated[bool, typer.Option("--uppercase", "-u")] = False,
    reverse: Annotated[bool, typer.Option("--reverse", "-r")] = False
):
    """Traite un fichier avec pipeline de transformations."""
    from pathlib import Path
    
    # Lire le contenu
    content = Path(input_file).read_text()
    
    # Pipeline de transformations
    if uppercase:
        typer.echo("Applying uppercase...")
        content = content.upper()
    
    if reverse:
        typer.echo("Applying reverse...")
        content = content[::-1]
    
    # Écrire le résultat
    if output_file:
        Path(output_file).write_text(content)
        typer.secho(f"[OK] Written to {output_file}", fg=typer.colors.GREEN)
    else:
        typer.echo(content)

if __name__ == "__main__":
    app()


# === Métriques et statistiques ===
import typer
from datetime import datetime
from pathlib import Path
import json

STATS_FILE = Path("stats.json")

def load_stats() -> dict:
    """Charge les statistiques."""
    if not STATS_FILE.exists():
        return {"commands": {}, "total_calls": 0}
    return json.loads(STATS_FILE.read_text())

def save_stats(stats: dict):
    """Sauvegarde les statistiques."""
    STATS_FILE.write_text(json.dumps(stats, indent=2))

def track_command(command_name: str):
    """Enregistre l'utilisation d'une commande."""
    stats = load_stats()
    stats["total_calls"] += 1
    if command_name not in stats["commands"]:
        stats["commands"][command_name] = {
            "count": 0,
            "last_used": None
        }
    stats["commands"][command_name]["count"] += 1
    stats["commands"][command_name]["last_used"] = datetime.now().isoformat()
    save_stats(stats)

app = typer.Typer()

@app.command()
def hello(name: str):
    """Dit bonjour."""
    track_command("hello")
    typer.echo(f"Hello {name}!")

@app.command()
def stats():
    """Affiche les statistiques d'utilisation."""
    data = load_stats()
    
    typer.secho("=== Usage Statistics ===", bold=True)
    typer.echo(f"Total calls: {data['total_calls']}")
    typer.echo("\nCommands:")
    
    for cmd, info in data["commands"].items():
        typer.echo(f"  {cmd}: {info['count']} calls")
        if info['last_used']:
            typer.echo(f"    Last used: {info['last_used']}")

if __name__ == "__main__":
    app()


# === Commandes avec timeout ===
import typer
from typing_extensions import Annotated
import signal
from contextlib import contextmanager

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError()

@contextmanager
def timeout(seconds: int):
    """Context manager pour timeout."""
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(seconds)
    try:
        yield
    finally:
        signal.alarm(0)

app = typer.Typer()

@app.command()
def long_task(
    max_seconds: Annotated[int, typer.Option()] = 10
):
    """Tâche longue avec timeout."""
    try:
        with timeout(max_seconds):
            typer.echo(f"Starting task (timeout: {max_seconds}s)...")
            import time
            time.sleep(15)  # Simule une tâche longue
            typer.echo("Task completed!")
    except TimeoutError:
        typer.secho(f"Task timed out after {max_seconds}s", fg=typer.colors.RED)
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()


# === Commandes avec retry ===
import typer
from typing_extensions import Annotated
import time
from functools import wraps

def retry(max_attempts: int = 3, delay: float = 1.0):
    """Décorateu pour réessayer en cas d'échec."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        typer.secho(
                            f"Failed after {max_attempts} attempts",
                            fg=typer.colors.RED
                        )
                        raise
                    
                    typer.secho(
                        f"Attempt {attempt} failed: {e}. Retrying in {delay}s...",
                        fg=typer.colors.YELLOW
                    )
                    time.sleep(delay)
        return wrapper
    return decorator

app = typer.Typer()

@app.command()
@retry(max_attempts=3, delay=2.0)
def unreliable_task():
    """Tâche qui peut échouer."""
    import random
    if random.random() < 0.7:  # 70% de chance d'échec
        raise Exception("Random failure")
    typer.secho("[OK] Task succeeded!", fg=typer.colors.GREEN)

if __name__ == "__main__":
    app()


# === Commandes avec cache ===
import typer
from pathlib import Path
import json
import hashlib
from datetime import datetime, timedelta

CACHE_DIR = Path(".cache")
CACHE_DIR.mkdir(exist_ok=True)

def cache_result(ttl_seconds: int = 3600):
    """Décorateu pour mettre en cache les résultats."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Créer une clé de cache
            cache_key = hashlib.md5(
                f"{func.__name__}{args}{kwargs}".encode()
            ).hexdigest()
            cache_file = CACHE_DIR / f"{cache_key}.json"
            
            # Vérifier le cache
            if cache_file.exists():
                cache_data = json.loads(cache_file.read_text())
                cached_time = datetime.fromisoformat(cache_data["timestamp"])
                
                if datetime.now() - cached_time < timedelta(seconds=ttl_seconds):
                    typer.secho("[OK] Using cached result", fg=typer.colors.BLUE)
                    return cache_data["result"]
            
            # Exécuter la fonction
            result = func(*args, **kwargs)
            
            # Mettre en cache
            cache_data = {
                "timestamp": datetime.now().isoformat(),
                "result": result
            }
            cache_file.write_text(json.dumps(cache_data))
            
            return result
        return wrapper
    return decorator

app = typer.Typer()

@app.command()
@cache_result(ttl_seconds=60)
def expensive_operation(n: int):
    """Opération coûteuse (mise en cache)."""
    typer.echo("Computing result...")
    import time
    time.sleep(2)  # Simule une opération lente
    result = sum(range(n))
    typer.echo(f"Result: {result}")
    return result

if __name__ == "__main__":
    app()


[OK] PATTERNS AVANCÉS

# === Factory pattern pour CLI ===
from typing import Protocol
import typer

class Command(Protocol):
    """Interface pour les commandes."""
    def execute(self) -> None:
        ...

class BackupCommand:
    """Commande de backup."""
    def execute(self):
        typer.echo("Executing backup...")

class RestoreCommand:
    """Commande de restore."""
    def execute(self):
        typer.echo("Executing restore...")

class CommandFactory:
    """Factory pour créer des commandes."""
    _commands = {
        "backup": BackupCommand,
        "restore": RestoreCommand
    }
    
    @classmethod
    def create(cls, command_type: str) -> Command:
        if command_type not in cls._commands:
            raise ValueError(f"Unknown command: {command_type}")
        return cls._commands[command_type]()

app = typer.Typer()

@app.command()
def run(command_type: str):
    """Exécute une commande via factory."""
    try:
        command = CommandFactory.create(command_type)
        command.execute()
    except ValueError as e:
        typer.secho(str(e), fg=typer.colors.RED)
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()


# === Strategy pattern ===
from abc import ABC, abstractmethod
import typer

class OutputStrategy(ABC):
    """Interface pour les stratégies de sortie."""
    @abstractmethod
    def output(self, data: dict):
        pass

class JSONOutput(OutputStrategy):
    def output(self, data: dict):
        import json
        typer.echo(json.dumps(data, indent=2))

class TableOutput(OutputStrategy):
    def output(self, data: dict):
        for key, value in data.items():
            typer.echo(f"{key}: {value}")

class CSVOutput(OutputStrategy):
    def output(self, data: dict):
        typer.echo(",".join(data.keys()))
        typer.echo(",".join(str(v) for v in data.values()))

app = typer.Typer()

@app.command()
def show(format: str = "json"):
    """Affiche des données dans différents formats."""
    data = {"name": "Alice", "age": 30, "city": "Paris"}
    
    strategies = {
        "json": JSONOutput(),
        "table": TableOutput(),
        "csv": CSVOutput()
    }
    
    if format not in strategies:
        typer.secho(f"Unknown format: {format}", fg=typer.colors.RED)
        raise typer.Exit(code=1)
    
    strategies[format].output(data)

if __name__ == "__main__":
    app()


# === Observer pattern pour événements ===
from typing import List, Callable
import typer

class EventBus:
    """Bus d'événements."""
    def __init__(self):
        self._listeners: dict[str, List[Callable]] = {}
    
    def subscribe(self, event: str, callback: Callable):
        """S'abonne à un événement."""
        if event not in self._listeners:
            self._listeners[event] = []
        self._listeners[event].append(callback)
    
    def publish(self, event: str, data: any = None):
        """Publie un événement."""
        if event in self._listeners:
            for callback in self._listeners[event]:
                callback(data)

# Bus global
event_bus = EventBus()

# Listeners
def on_user_created(data):
    typer.secho(f"[EMAIL] Sending welcome email to {data['email']}", fg=typer.colors.BLUE)

def on_user_created_log(data):
    typer.echo(f"[NOTE] Logging user creation: {data['username']}")

# S'abonner aux événements
event_bus.subscribe("user_created", on_user_created)
event_bus.subscribe("user_created", on_user_created_log)

app = typer.Typer()

@app.command()
def create_user(username: str, email: str):
    """Crée un utilisateur et déclenche des événements."""
    typer.echo(f"Creating user: {username}")
    
    # Publier l'événement
    event_bus.publish("user_created", {
        "username": username,
        "email": email
    })
    
    typer.secho("[OK] User created", fg=typer.colors.GREEN)

if __name__ == "__main__":
    app()


[OK] SÉCURITÉ

# === Validation des entrées ===
import typer
import re
from typing_extensions import Annotated

def validate_username(username: str) -> str:
    """Valide le nom d'utilisateur."""
    if not re.match(r'^[a-zA-Z0-9_-]{3,20}, username):
        raise typer.BadParameter(
            "Username must be 3-20 characters (letters, numbers, _, -)"
        )
    return username

def validate_email(email: str) -> str:
    """Valide l'adresse email.