19
Introduction à FastAPI (Python) : Partie 2
Voici une série d'articles qui vous permettra de créer une API en Python avec FastAPI.
Je vais publier un nouvel article environ aux deux jours et petit à petit vous apprendrez tout ce qu'il y a à savoir sur FastAPI
Pour ne rien manquer suivez-moi sur twitter : https://twitter.com/EricLeCodeur
Maintenant nous allons créer un API qui se rapprochera plus de ce que vous devrez créer dans vos projets.
CRUD est un acronyme qui signifie Create, Read, Update et Delete. Ces actions sont les actions le plus souvent utilisées lorsque l'on manipule des données.
Voici un exemple concret. Prenons un tableau de données contentant des produits :
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
Vous pourriez donc avoir des chemins URL pour exécuter des actions CRUD sur ce tableau de produit.
Voici quelques exemples:
Créer un nouveau produit
POST [www.example.com/](http://www.example.com/customers)products
Lire tous les produits
GET [www.example.com/products](http://www.example.com/customers/3814)
Lire un produit en particulier (ex. avec id = 2)
GET [www.example.com/products/](http://www.example.com/customers/3814)2
Modifier un produit en particulier (ex. avec id = 2)
PUT [www.example.com/products/](http://www.example.com/customers/3814)2
Effacer un produit en particulier (ex. avec id = 2)
DELETE [www.example.com/products/](http://www.example.com/customer/3814)2
À noter que le nom et la structure des chemins URL ne sont pas aléatoires. C'est une convention qui est utilisée dans la création d'API.
C'est pourquoi pour récupérer un produit en particulier vous devez spécifier sont id directement dans le path :
GET [www.example.com/products/](http://www.example.com/customers/3814)2
FastAPI permet justement de lire ce chemin URL et d'en extraire les informations pertinentes. Nous verrons ce concept sous peu.
Dans votre fichier first-api.py remplacer le contenu actuel avec celui-ci
from fastapi import FastAPI
app = FastAPI()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
Pour lancer le serveur et tester votre API, saisir dans le terminal (si ce n'est pas déjà fait).
$ uvicorn first-api:app --reload
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Vous pouvez donc ensuite visiter : http ://127.0.0.1:8000/products
La liste de tous les produits s'affichera en format JSON :
[
{
"id": 1,
"name": "iPad",
"price": 599
},
{
"id": 2,
"name": "iPhone",
"price": 999
},
{
"id": 3,
"name": "iWatch",
"price": 699
}
]
Nous avons donc créé le READ de notre API CRUD. Voyons maintenant les autres chemins URL
Pour lire tous un produit en particulier nous avons besoin d'extraire le id du chemin url. Par exemple avec le chemin "/products/2" comment extraire le 2 ?
FastAPI permet d'envoyer automatiquement une partie du path dans une variable
@app.get("/products/{id}")
def index(id: int):
for product in products:
if product["id"] == id:
return product
return "Not found"
Dans le @app.get() la partie représenter par {id} sera envoyée dans la variable "id" de la fonction index(id: int)
Il est ensuite possible d'utiliser cette variable "id" pour retrouver le bon produit.
À noter que le paramètre "id" est complété avec ":int" Cet ajout permet de spécifier le type de variable, dans ce cas-ci un integer.
Pourquoi utiliser un type dans le paramètre ? Cela permet à FastAPI de valider la donnée entrante.
Par exemple le chemin "/products/abc" retournerait une erreur car "abc" n'est pas un integer
Lorsque le serveur HTTP retourne une réponse, il renvoie toujours un code d'état (status code) avec la réponse.
Tous les codes d'état de réponse HTTP sont séparés en cinq classes ou catégories. Le premier chiffre du code d'état définit la classe de réponse, tandis que les deux derniers chiffres n'ont aucun rôle de classement ou de catégorisation. Il existe cinq classes définies par la norme :
1xx réponse d'information - la demande a été reçue, processus en cours
2xx réussi - la demande a été reçue, comprise et acceptée avec succès
Redirection 3xx - des mesures supplémentaires doivent être prises afin de compléter la demande
Erreur client 4xx - la demande contient une mauvaise syntaxe ou ne peut pas être satisfaite
Erreur de serveur 5xx - le serveur n'a pas réussi à répondre à une demande apparemment valide
Voici quelques exemples de code d'état
200 OK
201 Created
403 Forbidden
404 Not Found
500 Internal Server Error
Dans le dernier exemple FastAPI, si le produit n'est pas trouvé le chemin retournera "Not found" par contre le code d'état retourné sera toujours "200 OK"
Par convention quand une ressource n'est pas trouvée il faut retourner un status "404 Not Found"
FastAPI nous permet de modifier le code statut de la réponse
from fastapi import FastAPI, Response
...
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
Pour ce faire il faut ajouter 3 lignes à notre code :
- Premièrement nous devons importer l'objet Response
- Ensuite ajouter le paramètre "response : Response" à notre fonction
- Et enfin changer le statut pour 404 si le produit n'est pas trouvé
À noter que le paramètre "response : Response" peut vous sembler étrange, en effet comment est-ce possible que la variable "response" contienne une instance de l'objet "Response" sans même que nous ayons créé cette instance ?
C'est possible parce que FastAPI crée l'instance pour nous en arrière-plan. Cette technique s'appelle "Dependency Injection".
Pas besoin de comprendre ce concept, simplement l'utiliser c'est suffisant.
Prenons par exemple le chemin suivant :
/products/search/?name=iPhone
Ce chemin demande la liste de tous les produits qui contient le mot "iPhone"
Le ?search="iPhone" est un Query Parameter.
FastAPI nous permet d'extraire cette variable du chemin URL.
À la suite du code existant, saisir :
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
Il suffit d'ajouter le nom de la variable comme paramètre à la fonction index(). FastAPI va associer les Query Parameter automatiquement aux variables du même nom. Donc "?name=iPhone" se retrouvera dans le paramètre/variable "name"
Si vous lancez votre serveur et que vous essayez de visiter le chemin URL suivant
http://127.0.0.1:8000/products/search?name=iPhone
Vous aurez sans doute cette erreur
{
"detail": [
{
"loc": [
"path",
"id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
Pourquoi ? Le message d'erreur stipule que la valeur n'est pas de type integer ? Pourtant si on regarde notre fonction, aucune valeur n'est de type integer ? Nous avons seulement un paramètre "name"
En fait, le message dit bien la vérité. Voici tout notre code jusqu’à présent
from fastapi import FastAPI, Response
app = FastAPI()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
Nous avons une route "/products/{id}" qui est déclaré avant la dernière route. La partie dynamique de la route "{id}" signifie que toutes les routes qui correspondent à "/products/*" vont être exécutées avec ce code.
Donc lorsque nous demandons "/products/search/?name=iPhone" FastAPI nous envoi vers la deuxième route car elle correspond à "/products/*". La dernière fonction n'est jamais exécutée et ne le sera jamais.
La solution ? Inverser les routes, l'ordre des routes est primordial pour FastAPI. il est donc important de placer les routes dynamiques comme "/products/{id}" en dernier
from fastapi import FastAPI, Response, responses
app = FastAPI()
products = [
{"id": 1, "name": "iPad", "price": 599},
{"id": 2, "name": "iPhone", "price": 999},
{"id": 3, "name": "iWatch", "price": 699},
]
@app.get("/products")
def index():
return products
@app.get("/products/search")
def index(name, response: Response):
founded_products = [product for product in products if name.lower() in product["name"].lower()]
if not founded_products:
response.status_code = 404
return "No Products Found"
return founded_products if len(founded_products) > 1 else founded_products[0]
@app.get("/products/{id}")
def index(id: int, response: Response):
for product in products:
if product["id"] == id:
return product
response.status_code = 404
return "Product Not found"
Avec le code dans cet ordre, si vous revisitez "/products/search?name=iphone". Vous aurez la réponse suivante :
{
"id": 2,
"name": "iPhone",
"price": 999
}
C'est tout pour aujourd'hui, suivez-moi sur twitter : https://twitter.com/EricLeCodeur afin d'être avisé de la parution du prochain article (d'ici deux jours).
19