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_urlet 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.
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 :
# 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
_getest 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
Nonene 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
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
# 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ère | Import direct | Client HTTP |
|---|---|---|
| Couplage | Fort — même code, même machine | Faible — communication par contrat HTTP |
| Déploiement | Obligatoirement sur la même machine | Séparation possible (conteneurs, serveurs distincts) |
| Performance | Maximale — pas de réseau | Légère latence HTTP (négligeable en local) |
| Résilience | Pas de gestion réseau nécessaire | Gestion de timeout, erreurs réseau, retry |
| Sécurité | Accès direct à la base de données | API comme point d'entrée unique, clé d'API |
| Cas d'usage | Prototype, outil interne simple | Production, 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
_getprivé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