Avancé

Structurer le client HTTP

La fonction api_get centralise la gestion d'erreurs et l'authentification. Quand le nombre d'endpoints grandit, il est préférable de structurer le code dans une classe client dédiée.

Le problème avec api_get seul

api_get gère les erreurs et les headers, mais elle a des limites :

  • La base_url et la clé API sont dispersées dans le code
  • Chaque appel recrée une connexion TCP (pas de réutilisation)
  • Les méthodes métier (ventes par région, évolution mensuelle...) n'ont pas d'interface claire

requests.Session : connexions réutilisables

requests.Session() réutilise la même connexion TCP pour plusieurs requêtes vers le même serveur. Les headers définis sur la session sont inclus dans chaque requête.

python
import requests

session = requests.Session()
session.headers.update({
    "X-API-Key": "ma-cle-secrete",
    "Accept": "application/json"
})

# Tous les appels via cette session incluent automatiquement les headers
response = session.get("http://localhost:5000/api/v1/ventes/par-region")

Avantages :

  • Performance : réutilisation de la connexion TCP (pas de nouvelle connexion à chaque requête)
  • Headers persistants : configurés une seule fois, envoyés à chaque requête
  • Configuration centralisée : un seul endroit pour la base URL et l'authentification

Quel est le principal avantage de requests.Session() par rapport à requests.get() direct ?

La classe VentesAPIClient

Une classe client qui encapsule toute la communication HTTP :

python
# dashboard/api_client.py
import requests
import pandas as pd
import streamlit as st


class VentesAPIClient:
    """Client HTTP pour l'API de ventes."""

    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "X-API-Key": api_key,
            "Accept": "application/json"
        })

    def _get(self, endpoint: str, params: dict = None) -> dict | None:
        """Appel GET interne avec gestion d'erreurs."""
        try:
            response = self.session.get(
                f"{self.base_url}/{endpoint}",
                params=params,
                timeout=10
            )
            response.raise_for_status()
            return response.json()
        except requests.exceptions.ConnectionError:
            st.error("API inaccessible. Vérifiez que le serveur Flask est lancé.")
            return None
        except requests.exceptions.Timeout:
            st.error("L'API ne répond pas dans les délais.")
            return None
        except requests.exceptions.HTTPError:
            code = response.status_code
            if code == 401:
                st.error("Clé API invalide.")
            elif code == 404:
                st.warning("Aucune donnée trouvée.")
            else:
                st.error(f"Erreur API (code {code}).")
            return None
        except Exception as e:
            st.error(f"Erreur inattendue : {e}")
            return None

    def get_par_region(self, annee: int = None) -> pd.DataFrame:
        """Récupère les ventes par région."""
        params = {"annee": annee} if annee else None
        data = self._get("ventes/par-region", params=params)
        return pd.DataFrame(data["data"]) if data else pd.DataFrame()

    def get_evolution_mensuelle(
        self, region: str = None, annee: int = None
    ) -> pd.DataFrame:
        """Récupère l'évolution mensuelle des ventes."""
        params = {}
        if region:
            params["region"] = region
        if annee:
            params["annee"] = annee
        data = self._get("ventes/evolution", params=params or None)
        return pd.DataFrame(data["data"]) if data else pd.DataFrame()

    def get_stats(self, region: str = None, annee: int = None) -> dict:
        """Récupère les statistiques agrégées."""
        params = {}
        if region:
            params["region"] = region
        if annee:
            params["annee"] = annee
        data = self._get("ventes/stats", params=params or None)
        return data or {}

Points de conception importants

  • _get est privée (préfixe _) : seul le client l'utilise en interne
  • Les méthodes publiques retournent des DataFrames : l'interface est claire pour le code Streamlit
  • DataFrame vide en cas d'erreur : le code appelant peut toujours vérifier avec df.empty
  • Paramètres optionnels : les filtres None ne sont pas envoyés à l'API

Pourquoi les méthodes publiques du client retournent-elles un DataFrame (ou dict) plutôt que le JSON brut ?

Utilisation dans Streamlit

Instancier le client avec @st.cache_resource

python
import streamlit as st
from api_client import VentesAPIClient


@st.cache_resource
def get_client():
    """Crée le client API une seule fois (mis en cache par Streamlit)."""
    return VentesAPIClient(
        base_url=st.secrets["API_BASE_URL"],
        api_key=st.secrets["API_KEY"]
    )


client = get_client()

Pourquoi @st.cache_resource ?

@st.cache_resource garantit que le client est créé une seule fois, même si Streamlit ré-exécute le script à chaque interaction. La connexion TCP de la Session est réutilisée entre les recharges — plus performant.

Appels dans le dashboard

python
# Filtre par année
annee = st.selectbox("Année", [2024, 2023, 2022])

# Appels API — même interface que le repository !
df_regions = client.get_par_region(annee=annee)
df_evolution = client.get_evolution_mensuelle(annee=annee)
stats = client.get_stats(annee=annee)

# Affichage
if not df_regions.empty:
    fig = px.bar(df_regions, x="region", y="chiffre_affaires")
    st.plotly_chart(fig, use_container_width=True)

if stats:
    st.metric("CA Total", f"{stats.get('total_ca', 0):,.0f} €")

L'interface est presque identique à celle du repository direct (repo.get_par_region(annee=annee)). Seule la source des données change — de l'import direct à HTTP — mais le code Streamlit reste le même.

Comparaison : import direct vs client HTTP

CritèreImport directClient HTTP
CouplageFort — même code, même machineFaible — communication par contrat HTTP
DéploiementObligatoirement sur la même machineSéparation possible (conteneurs, serveurs distincts)
PerformanceMaximale — pas de réseauLégère latence HTTP (négligeable en local)
RésiliencePas de gestion réseau nécessaireGestion de timeout, erreurs réseau, retry
SécuritéAccès direct à la base de donnéesAPI comme point d'entrée unique, clé d'API
Cas d'usagePrototype, outil interne simpleProduction, clients multiples

Quel décorateur Streamlit utiliser pour que le VentesAPIClient ne soit créé qu'une seule fois ?

À retenir

Points clés

  • requests.Session() réutilise les connexions TCP et conserve les headers
  • Classe VentesAPIClient : encapsule toute la communication HTTP dans un seul fichier
  • Méthode _get privée : gestion d'erreurs centralisée
  • Méthodes publiques : retournent des DataFrames prêts à l'emploi
  • @st.cache_resource : le client est créé une seule fois et partagé entre les rechargements
  • L'interface du client HTTP est quasi identique à celle du repository direct

Ressources