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

Créer un API CRUD

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.

Premier pas avec API CRUD

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

Extraire le "id" du chemin 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

Status Code

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.

Extraire les "Query Parameters"

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"

Ordre de déclaration des chemins URL

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
}

Conclusion

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).

20