Séparer les responsabilités
- Leçon 6
- 40 min
- Module 2 : Créer un petit service FastAPI
Leçon 6 sur 21 · Créer et déployer votre première APISéparer les responsabilités
1. Comprendre la requête Web
2. Créer un petit service FastAPI
3. Concevoir un contrat utilisable
4. Documenter l'API avec OpenAPI et Swagger UI
5. Explorer et tester avec Postman
6. Tester et renforcer le service
7. Déployer et vérifier
Ce que vous saurez faire
Divisez l’API en parties qui ont chacune une tâche : la couche HTTP, les modèles de données et le classificateur. Les cours suivants remplacent le classificateur à mots-clés par un modèle entraîné. Avec cette structure, ce changement touche un seul module. L’API qui l’entoure reste la même.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les modules et les importations, présentés dans Modules et importations : exemple détaillé : diviser le script.
- Les modèles
TicketInetClassification, présentés dans Modèles de requête et de réponse : les modèles décrivent les données.
Ce qu’il vous faut sur votre ordinateur
ticket_api/main.pyavec les modèles, les règles à mots-clés etPOST /classify, de Modèles de requête et de réponse : exemple détaillé : le point de terminaison de classification. Si vous avez ajoutéchanneldans l’exercice « À vous de jouer », retirez-le, comme l’indique l’exercice. Si votre fichier est différent ou ne fonctionne plus, remplacez-le de nouveau par l’exemple détaillé.- Le dossier
requestsdans le projet, avecbilling.jsonetempty-subject.json. Vous l’avez créé dans Modèles de requête et de réponse : exercice guidé : créer et appeler le point de terminaison. Le tableau dans Requêtes valides et invalides contient le contenu de chaque fichier. - Le serveur de développement fonctionne, comme dans Environnement et premier point de terminaison : étape 3 : démarrer le serveur.
- Si le projet manque, recréez-le : faites Environnement et premier point de terminaison, puis l’exercice guidé de Modèles de requête et de réponse.
Testé avec Python 3.14.6 et FastAPI 0.142.2 sur macOS, et Pyodide 314.0 (navigateur).
Trois responsabilités, trois modules
Section intitulée « Trois responsabilités, trois modules »Une responsabilité est un type de tâche. main.py en mélange actuellement trois :
| Responsabilité | Question à laquelle elle répond | Module |
|---|---|---|
| HTTP | Quelle méthode et quel chemin? Quel code d’état? | routes.py |
| Données | À quoi ressemblent un billet de soutien valide et un résultat valide? | models.py |
| Classification | Quelle catégorie et quelle priorité pour ce texte? | classifier.py |
Le classificateur ne doit rien savoir de HTTP. Il reçoit du texte et retourne une prédiction, comme les fonctions pures dans Le contrat d’une fonction. Vous pouvez ainsi le vérifier sans serveur et le remplacer sans modifier les routes. main.py sert seulement à assembler les parties pour créer l’application.
Exemple détaillé : le module du classificateur
Section intitulée « Exemple détaillé : le module du classificateur »Le classificateur reçoit aussi de meilleures règles. Il compte les mots-clés de chaque catégorie et choisit celle qui en a le plus. Il marque un billet comme urgent (priorité 1) quand celui-ci contient un mot qui signale une urgence. Le résultat est une petite dataclass (classe de données), Prediction, sans HTTP ni Pydantic.
from dataclasses import dataclass
KEYWORDS = { "billing": ("invoice", "charged", "refund", "payment"), "login": ("password", "sign in", "log in", "two-factor", "locked out"), "shipping": ("parcel", "delivery", "courier", "tracking"), "account": ("account", "company name", "user"),}URGENT_WORDS = ("cannot", "locked out", "charged twice", "lost")
@dataclass(frozen=True)class Prediction: category: str priority: int confidence: float
class KeywordClassifier: """A deterministic stand-in for a trained model: the same text always gives the same prediction."""
version = "keywords-1.0"
def predict(self, text: str) -> Prediction: lowered = text.lower() scores = { category: sum(word in lowered for word in words) for category, words in KEYWORDS.items() } best = max(scores, key=scores.get) if scores[best] == 0: return Prediction(category="other", priority=3, confidence=0.2) urgent = any(word in lowered for word in URGENT_WORDS) confidence = min(0.5 + 0.2 * scores[best], 0.95) return Prediction( category=best, priority=1 if urgent else 2, confidence=round(confidence, 2) )| Partie | Signification |
|---|---|
sum(word in lowered for word in words) |
Compte les mots-clés d’une catégorie dans le texte. True (vrai) compte pour 1. |
max(scores, key=scores.get) |
La catégorie avec le compte le plus élevé. En cas d’égalité, la première dans KEYWORDS gagne. Le résultat reste donc toujours le même. |
confidence |
0,7 pour un mot-clé, 0,9 pour deux, jamais plus de 0,95. Un modèle entraîné donne une vraie probabilité. Celui-ci en imite seulement la forme. |
version |
Chaque réponse indique quel classificateur l’a produite. Quand le classificateur change, les clients peuvent le voir. |
Le classificateur utilise seulement Python. Vous pouvez donc l’essayer ici même :
Exécutez du Python dans votre navigateur
Prédisez les résultats, puis exécutez le classificateur
Avant de l’exécuter, prédisez les trois résultats. Le deuxième texte contient un mot lié à la facturation et un mot lié à l’expédition. Quelle catégorie gagne, et pourquoi? Ajoutez ensuite votre propre texte.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute le code.
Dépendances : fournir son classificateur à la route
Section intitulée « Dépendances : fournir son classificateur à la route »La route a besoin d’un objet classificateur. Elle pourrait en créer un elle-même ou importer un objet global. Mais un test, ou une version future avec un vrai modèle, ne pourrait alors pas lui en fournir un autre. FastAPI offre une meilleure façon : une dépendance. Vous écrivez une fonction qui fournit l’objet, et la route le demande avec Depends :
from typing import Annotated
from fastapi import APIRouter, Depends, Request
from ticket_api.classifier import KeywordClassifierfrom ticket_api.models import Classification, TicketIn
router = APIRouter()
def get_classifier(request: Request) -> KeywordClassifier: return request.app.state.classifier
@router.post("/classify")def classify( ticket: TicketIn, classifier: Annotated[KeywordClassifier, Depends(get_classifier)],) -> Classification: prediction = classifier.predict(f"{ticket.subject}\n{ticket.body}") return Classification( category=prediction.category, priority=prediction.priority, confidence=prediction.confidence, model_version=classifier.version, )| Partie | Signification |
|---|---|
APIRouter() |
Un groupe de routes dans son propre module. main.py l’ajoute à l’application. |
get_classifier(request) |
La dépendance. Elle retourne l’unique classificateur que l’application garde dans app.state. |
Annotated[KeywordClassifier, Depends(get_classifier)] |
« Ce paramètre est un KeywordClassifier, et FastAPI l’obtient en appelant get_classifier. » |
Pour chaque requête, FastAPI appelle get_classifier et transmet le résultat à classify. La route ne sait pas d’où vient le classificateur. C’est l’injection de dépendances : l’appelant fournit ce dont une fonction a besoin, plutôt que la fonction le crée. Dans le module 6, un test l’utilise pour fournir à la route un classificateur volontairement en panne.
Les modèles passent dans models.py sans changement, et main.py devient court :
from fastapi import FastAPI
from ticket_api.classifier import KeywordClassifierfrom ticket_api.routes import router
app = FastAPI(title="Ticket Classifier API")app.state.classifier = KeywordClassifier()app.include_router(router)
@app.get("/health")def health() -> dict[str, str]: return {"status": "ok"}app.state.classifier = KeywordClassifier() crée le classificateur une seule fois, au démarrage de l’application. Un modèle entraîné peut prendre plusieurs secondes à charger. Il ne doit donc pas se recharger à chaque requête.
Description textuelle du diagramme
Une requête POST /classify arrive avec un corps JSON. FastAPI vérifie le corps selon TicketIn. S’il est invalide, FastAPI répond 422 avec tous les problèmes, et la route ne s’exécute jamais. S’il est valide, FastAPI appelle get_classifier pour obtenir l’objet classificateur. Ensuite, il appelle la fonction de route classify dans routes.py avec le billet et le classificateur. La route envoie le texte à KeywordClassifier.predict dans classifier.py, qui retourne une Prediction. La route la transforme en Classification, et FastAPI l’envoie comme réponse JSON 200.
Exercice guidé : diviser le projet
Section intitulée « Exercice guidé : diviser le projet »-
Créez
ticket_api/classifier.pyavec le code de l’exemple détaillé. -
Créez
ticket_api/models.py. Déplacez-yCategory,TicketInetClassificationdepuismain.py, avec les importations nécessaires (Literal,BaseModel,Field). -
Créez
ticket_api/routes.pyavec le code ci-dessus. -
Remplacez
ticket_api/main.pypar la version courte. Supprimez-enKEYWORDS: le classificateur a maintenant sa propre copie. -
Enregistrez et vérifiez le terminal du serveur. S’il affiche une
ImportError(erreur d’importation), comparez les lignes d’importation avec le code ci-dessus. -
Envoyez de nouveau
requests/billing.json:Sortie {"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}Le résultat diffère de la première version du module 2 : deux mots de facturation donnent
0.9. L’expression « charged twice » (facturé deux fois) rend le billet urgent. -
Envoyez
requests/empty-subject.json. Il donne encore422. Les modèles n’ont pas changé, seulement leur module.
À vous de jouer : un point de terminaison de classification avec un schéma documenté
Section intitulée « À vous de jouer : un point de terminaison de classification avec un schéma documenté »Le service de soutien veut une nouvelle catégorie, refund (remboursement), distincte de billing (facturation). Elle doit reconnaître les mots refund et money back (remboursement). Faites le changement dans un seul module, et choisissez lequel. Ajoutez refund à la fin de KEYWORDS. Ensuite :
- Prédisez le résultat pour
{"subject": "Refund for the lost parcel"}, et vérifiez-le avec curl. - Prédisez le résultat pour
{"subject": "I want my money back"}, et vérifiez-le. - Ouvrez
http://127.0.0.1:8000/docsdans votre navigateur et trouvezClassificationsous Schemas (schémas), au bas de la page. Est-ce querefundfigure dans sa liste de catégories?
Afficher la réponse
Les mots-clés appartiennent au classificateur. C’est donc classifier.py qui change : ajoutez "refund": ("refund", "money back") et retirez "refund" de billing.
shipping(expédition), priorité1, état200. Le texte contient un mot deshipping(parcel, colis) et un mot derefund. Il y a égalité, etshippingvient en premier dansKEYWORDS, donc cette catégorie gagne.lost(perdu) rend le billet urgent.500 Internal Server Error(erreur interne du serveur).refundgagne, maisClassificationaccepte seulement les cinq valeurs deCategory. Pydantic refuse donc le résultat, comme dans l’erreur fréquente de la leçon précédente.- Non. Le schéma présente encore les cinq anciennes catégories.
Un deuxième module doit donc changer : ajoutez "refund" à Category dans models.py. Une nouvelle valeur de sortie change le contrat avec les clients. Il est donc normal de devoir modifier models.py, et pas seulement le classificateur. Annulez les deux changements avant le prochain module. Le reste du cours utilise les cinq catégories.
Erreur courante
ImportError après avoir déplacé du code
Symptôme : après avoir déplacé du code dans un nouveau module, le terminal du serveur affiche une erreur. Chaque requête échoue avec Couldn't connect to server (connexion au serveur impossible) :
ImportError: cannot import name 'TicketIn' from 'ticket_api.routes'ou NameError: name 'Field' is not defined (le nom « Field » n’est pas défini).
Cause : une ligne d’importation pointe encore vers l’ancien module, ou le nouveau module utilise un nom sans l’importer. Le serveur de développement ne peut pas charger l’application. Rien n’écoute donc sur le port.
Solution :
- Lisez la dernière ligne de l’erreur : quel nom et quel module indique-t-elle?
- Vérifiez que le nom est défini dans le module indiqué par la ligne d’importation.
TicketInse trouve maintenant dansticket_api.models. - Vérifiez que chaque module importe tous les noms qu’il utilise, par exemple
Fielddansmodels.py. - Enregistrez. Le serveur de développement réessaie automatiquement.
Vérifiez votre compréhension
Pourquoi le classificateur retourne-t-il une dataclass Prediction plutôt que le modèle Classification de models.py?
Afficher la réponse
Le classificateur ne doit pas dépendre de l’API. Classification fait partie du contrat HTTP : il contient le champ model_version et les limites sur lesquelles les clients comptent. Prediction est seulement la réponse du classificateur. Un nouveau classificateur peut être, par exemple, un modèle entraîné dans un cours suivant. En les gardant séparés, ce nouveau classificateur doit seulement retourner une Prediction. Le contrat avec les clients ne change pas.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification du module : envoyez un billet valide, un billet sans sujet et un billet avec un sujet de 200 caractères. Donnez le code d'état de chacun. Expliquez qui l'a décidé : FastAPI, votre route ou le classificateur.
Afficher la réponse
Le billet valide donne 200 : FastAPI a vérifié le corps, la route a interrogé le classificateur et retourné la Classification. Le sujet manquant donne 422, décidé par FastAPI selon TicketIn (missing, manquant, à ["body", "subject"]), avant l’exécution de la route. Le sujet de 200 caractères donne aussi 422, décidé par FastAPI (string_too_long, chaîne trop longue), à cause de max_length=120. Le classificateur décide seulement du contenu d’une réponse 200, jamais du code d’état.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Gardez chaque responsabilité dans son module : HTTP dans
routes.py, les données dansmodels.py, la classification dansclassifier.py.main.pyassemble l’application. - Le classificateur utilise seulement Python. Il ne sait rien de HTTP et retourne une
Prediction. - Une dépendance (
Depends) fournit à une route ce dont elle a besoin. La route ne le crée pas. - Créez les objets coûteux, comme un modèle, une seule fois au démarrage de l’application. Gardez-les dans
app.state.
Vous avez maintenant une API fonctionnelle et structurée. Dans le prochain module, vous concevez son contrat : des routes claires, un format d’erreur unique et un accès sécuritaire depuis le navigateur.
Leçon suivante : Conception des ressources et des points de terminaison