Webservices et formats de données

TP 3
Author

Rémi Pépin, Ludovic Deneuville

Avant de commencer

😱 Comme vous pouvez le constater, le sujet de ce TP est lui aussi long. Cela ne doit pas vous effrayer.

Il mélange explications complètes et manipulations pour être au maximum autosuffisant.

Vous n’allez surement pas terminer le sujet, ce n’est pas grave. Il est là pour vous aider lors du projet informatique.

Objectifs

Dans ce TP vous allez :

  • Faire des appels à un webservice à la main avec Insomnia
  • Faire des appels à un webservice avec la bibliothèque python requests
  • Découvrir la page swagger d’un webservice
  • Manipuler différents formats de données
  • Créer un webservice avec le framework python fastAPI

1 Appeler un webservice à la main

La première partie de ce TP ne nécessite pas d’écrire du code, mais seulement de faire des requêtes à un webservice en utilisant le logiciel Insomnia installé sur votre machine.

1.1 Webservices

Note

Webservice : le terme webservice est un terme vaste et il serait compliqué d’en donner une définition courte (article wikipedia).

Dans le cadre du projet un webservice désigne une application accessible via le protocole HTTP (HyperText Transfer Protocol) qui respecte généralement l’architecture REST (REpresentational State Transfer).

Mais il en existe d’autre comme SOAP (Simple Objet- Access Protocol) ou RPC (Remote Procedure Call)

En d’autres termes, un webservice est une application accessible via le web que l’on va pouvoir requêter soit :

  • pour obtenir des ressources
  • pour modifier les ressources accessibles.

Un webservice peut seulement avoir pour but d’être une point d’accès unique et normalisé à des données (comme une interface à une base de données), mais il peut également être une manière de contrôler un système d’informations (lancer des travaux, les mettre en attente, récupérer des résultats…)

Les webservices utilisent le protocole HTTP qui est le protocole du web (et pas d’internet). C’est celui que vous utilisez sans le savoir avec votre navigateur web. Requêter un webservice se fait presque comme requêter une page web.

Pour cela il vous faut :

  • l’adresse de la ressource, son Uniforme Resource Identifier (URI)
    • c’est une notion plus générale que les Uniforme Resource Locator (URL)
  • une méthode (GET, POST, PUT, DELETE, liste des méthodes)
  • et potentiellement des données

1.2 Premières requêtes GET

    • bouton Create à droite
    • puis cliquez sur votre collection
    • en appuyant sur CTRL+N
    • donnez lui un nom
    • vérifiez que c’est bien une requête de type GET

Dans la barre d’adresse, testez les requêtes des webservices ci-dessous :

  • Regardez la réponse dans la partie droite de votre écran
  • Quelles sont les similarités entre les réponses ?

1.2.1 Carbon intensity

Webservice sur les émissions carbone du Royaume-Uni

Requêtes à tester :

    • en remplaçant {date} par la date de votre choix au format YYYY-MM-DD

1.2.2 Rennes Métropole en accès libre

Webservice Rennes Métropole en accès libre

Récupérez les données de divers datasets :

    • Remplacez <dataset_id> par une des valeurs ci-après
    • pour faire varier le nombre de lignes que vous recevez
    • ajoutez simplement ?rows=N avec N le nombre de lignes

1.2.3 Webservice de votre projet

1.3 Requêtes avancées

Après avoir exploré quelques webservices externes, vous allez travaillez avec un webservice hébergé à l’ENSAI spécialement créé pour ces TP.

Toujours avec Insomnia :

    • web-services.domensai.ecole/attack
    • web-services.domensai.ecole/attack/{identifier}
    • en remplaçant {identifier} par le nom ou l’id d’une attaque que vous venez de récupérer
    • web-services.domensai.ecole/attack?type_attack_id={id_type}
    • en remplaçant {id_type} par un entier entre 1 et 4.
    • web-services.domensai.ecole/attack?type_attack_name={type attack}
    • en remplaçant {type attack} par special attack ou physical attack ou fixed damage ou status attack
    • web-services.domensai.ecole/attack
    • Cliquez sur Body, puis JSON, coller le texte ci-dessous, puis remplacez les valeurs des attributs pour créer votre propre attaque
    {
      "name": "An awesome name",
      "attack_type": "physical attack"/"physical attack"/"fixed damage"/"status attack",
      "power": 0,
      "accuracy": 0,
      "element": "An awesome element",
      "description": "An awesome description"
    }
    • web-services.domensai.ecole/attack/{identifier}
    • en remplaçant {identifier} par le nom ou l’id de l’attaque que vous venez de créer

1.4 Swagger

Dans votre navigateur web allez sur la page http://web-services.domensai.ecole/docs.

Cela vous amène sur la page swagger du webservice. Cette page recense tous les endpoints du webservice, et comment les utiliser. Essayez via l’interface de :

  • modifier une attaque
  • supprimer une attaque
  • afficher une liste de pokémon
  • ajouter un pokémon

2 Webservice en Python

Aujourd’hui, les plus grands consommateurs de webservices sont les machines. Nous allons voir comment automatiser des appels à un webservice en Python.

Note

Aujourd’hui beaucoup d’applications web (par exemple Facebook, Netflix, Dailymotion, Uber) utilisent ce que l’on appelle des architectures “micro services”.

Les échanges entre leurs composants applicatifs (par exemple entre leur interface homme machine (IHM) et leurs services internes) se font via des webservices à but unique. Cela permet d’avoir des modules découplés les uns des autres car ils communiquent uniquement via des requêtes HTTP, ou avec des systèmes de gestion d’évènements. Ils ont seulement à savoir comment ils doivent communiquer les uns avec les autres et pas le fonctionnement interne des autres modules.

Cela demande de bien documenter ses webservices et de gérer ÉNORMÉMENT d’applications en parallèle. Amazon, Google, Facebook peuvent se le permettre, par contre une petite entreprise de 10 employés non.

2.1 La bibliothèque requests

Le principe va rester le même que faire une requête à la main avec Insomnia. Vous allez utiliser la bibliothèque requests pour avoir seulement à remplir les parties intéressantes de vos requêtes.

Pour faire une requête GET vous allez simplement faire :

import requests

response = requests.get("http://mon-webservice.com") 

Exécuter cette ligne de code va :

  1. Envoyer la requête GET au serveur que vous contactez
  2. Stockez le résultat dans la variable response

Cette variable response est un objet, et comme tout objet elle a des attributs et des méthodes, par exemple :

  • response.text : le corps du résultat sous forme de string en laissant requests inférer l’encodage (cela fonctionne souvent)
    • Problème : vous avez un string, et ce n’est pas le meilleur format de données à manipuler
  • response.json() : le corps du résultat comme un dictionnaire.
    • C’est ce que vous allez faire le plus souvent car le format json est un format simple à manipuler
  • response.encoding : l’encoding de votre requête (utile en cas de problème d’encoding)
  • response.status_code : le statut de la requête. les principaux sont :
    • 200 : retour général pour dire que tout c’est bien passé
    • 201 : ressource créée avec succès
    • 202 : requête acceptée, sans garantie du résultat (par exemple dans un système asynchrone)
    • 400 : erreur de syntaxe dans la requête
    • 401 : erreur, une authentification est nécessaire
    • 403 : la ressource est interdite (droits insuffisants)
    • 404 : ressource non trouvée
    • 405 : une mauvaise méthode http a été utilisée
    • 500 : erreur côté serveur
    • 503 : service temporairement indisponible

Pour résumer, les résultats :

  • 2xx indiquent un succès
  • un résultat 4xx ou 5xx un problème

2.2 Exemples d’utilisation

Le principe sera toujours le même :

  1. Vous lancez la requête
  2. Vous vérifiez si le statut est OK
  3. Vous récupérez le json et traitez le résultat (affichage, convertion en dataframe…)

2.3 Mise à jour de votre dépôt git

Caution

Si vous n’avez pas le dépôt sur votre machine, créez un clone en suivant la section Clone du dépôt du TP2.

    • File > Open Folder
    • Allez dans le dossier P:\Cours2A\UE3-Complements-info\TP2
    • Cliquez une seule fois sur 📁 ENSAI-2A-complement-info-TP
    • Puis sur le bouton Sélectionner un dossier
Important
    • L’Explorer, à gauche, permet d’explorer l’arborsence des fichiers et dossiers

⚠️ Si le dossier parent n’est pas le bon, recommencez l’Open Folder où vous aurez de gros soucis par la suite !!!

    • Terminal > New Terminal (ou CTRL + ù)
    • Passez sur la branche du TP3 et mettez-là à jour
      • si vous ne l’avez pas fait à la fin du dernier TP, commencer par un add et un commit
      • git switch tp3-base
      • git pull
Important

Pour pour vérifier que tout fonctionne :

    • dans le terminal : python -m pytest -v src/tests/test_business_object/

S’il vous manque des packages, suivez les instructions du README.

2.4 Mes premières requêtes en Python

    • prend en paramètre un id d’attaque
    • va chercher toutes les informations disponibles sur cette attaque
    • retourne un objet de type AbstractAttack
Tip

Pour vous aider, observez la méthode instantiate_attack() de la classe AttackFactory (business_object).

Regardez le fonctionnement de cette méthode et utilisez la.

    • retourne la liste de tous les attaques disponibles sous la forme d’une liste d’objets AbstractAttack
    • Lancez les tests unitaires du package test_client

2.5 Les requêtes plus complexes

Pour le moment nous nous sommes concentrés sur les requêtes GET (simple lecture des données).

Mais il est bien sûr possible d’en faire d’autres. Par exemple pour les requêtes POST, PUT ou DELETE. Voici la syntaxe :

post = requests.post("http://example.org", json = {'key':'value'})
put = requests.put("http://example.org", json = {'key':'value'})
delete = requests.delete("http://example.org")

Comme vous le voyez, les syntaxes sont très proches de la syntaxe de la méthode GET. Nous avons seulement ajouté pour certaines requêtes des données. C’est ce que vous avez fait plus tôt avec Insomnia. Pour passer des paramètres à votre requête je vous conseille néanmoins de préférer ce genre de syntaxe :

url = "http://example.org"
data = {'key':'value'}
post = requests.post(url, json = data)

Fonctionnellement, c’est la même chose, mais il vaut mieux définir les éléments hors de la requête pour ne pas se perdre.

Il est également possible de passer des entêtes http en ajoutant le paramètre headers à la fonction utilisée.

headers = {'accept': 'application/xml'}
requests.get('http://example.org', headers=headers)

2.6 Requêtes avancées en python

Dans le module attack_client.py implémentez les méthodes suivantes :

    • prend une AbstractAttack en paramètre
    • crée une nouvelle ressource dans notre webservice
    • prend une AbstractAttack en paramètre
    • modifie la ressource associée dans notre webservice
    • prend une AbstractAttack en paramètre
    • supprime la ressource associée dans notre webservice

3 Codez un webservice en Python

Note

Jusqu’à maintenant vous avez utilisé des webservices existants. Maintenant vous allez créer votre propre webservice.

Avec les outils à disposition aujourd’hui, il est facile de faire un webservice soit même.

Il y a trois leaders sur le marché actuellement pour faire un webservice REST en Python :

Chacun à ses avantages et inconvénients. Django est sûrement le plus complet mais le plus lourd, Flask et FastApi sont plus légers et rapides à mettre en place. Le gros avantage de FastApi est la simplicité pour créer une page swagger de documentation.

Voici le code minimal d’un webservice REST avec FastAPI (documentation officielle)

from fastapi import FastAPI

# On instancie le webservice
app = FastAPI()

# Création d'un enpoint qui répond à la méthode GET à l'adresse "/" qui va retourne le message "Hello World"
@app.get("/")
async def root():
    return {"message": "Hello World"}

# Lancement de l'application sur le le port 80
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=80)

Appeler la ressource “/” du webservice va retourner le json : {"message": "Hello World"}

Voici un exemple plus complet inspiré de la documentation officielle (vous voulez créer un webservice pour exposer vos todos)

from fastapi import FastAPI
from pydantic import BaseModel
from starlette import status
import uvicorn

# On instancie le webservice
app = FastAPI()

class Todo(BaseModel):
    id : int
    content : str

todos = {1 : Todo(1,"Step 1 : Learn python")
        , 2 : Todo(2,"Step 2 : Work on the IT project")
        , 3 : Todo(3,"Step 3 : ???")
        , 4 : Todo(4,"Step 4 : Profit")}

# Définition du endpoint get /todo
@app.get("/todo")
async def get_all_todo():
    return todos.values()

# Définition du endpoint get /todo/{id_doto}
@app.get("/todo/{id_toto}")
async def get_todo_by_id(id_toto : int = Path(..., description = "The `id` of the todo you want to get")):
    if todos.get[id_toto] :
        return todos.get[id_toto]
    else :
        return JSONResponse(status_code=status.HTTP_404_NOT_FOUND)

# Définition du endpoint post /todo
@app.post("/todo", todo, status_code=201)
async def post_todo(todo:Todo):
    if not todos.get(todo.id):
        return JSONResponse(status_code=status.HTTP_409_CONFLICT)
    else :
        todos[todo.id] = todo
        return todo

# Lancement de l'application sur le le port 8XXX avec XXX les 3 derniers numéros de votre id
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8XXX)

Ce code va créer un web service qui va répondre aux requêtes suivantes :

  • GET host/todo : retourne toutes les tâches à faire
  • GET host/todo/{todo_id} : retourne la tâche derrière l’id en paramètre
  • POST host/todo/ : ajoute la tâche passée en corps de la rêquete

FastAPI sérialise pour vous les objets que vous retournez. Donc pas besoin de mettre en forme vos données. Néanmoins, pour plus de clarté, vous pouvez utiliser des classes BaseModel. Ce sont des classes qui ne vont contenir que des attributs que vous pouvez déclarer sans constructeur :

class Todo(BaseModel):
    id : int
    content : str

Ces classes peuvent être utilisées en sortie de votre webservice, comme en entrée (ligne 33). FastApi va faire pour vous tout une série de contrôles sur les types des variables et renvoyer une erreur au client si sa requête n’est pas bien formatée.

Fondamentalement un webservice est une application comme les autres, mais au lieu d’avoir une interface graphique comme on en a l’habitude en tant qu’humain, l’interface est une interface HTTP qui va accepter des requêtes et envoyer des résultats. Ainsi le diagramme de séquence des différentes couches qui vont être impliquées dans une requête GET pour récupérer une ressource va ressembler à cela. Reprenons le modèle 3 couches vu en cours.

sequenceDiagram
    participant U as User
    participant R as Webservice
    participant S as Service
    participant D as DAO
    participant B as Base de données
    U ->> R : HTTP requête
    R ->> S : get_by_id()
    S ->> D : find_by_id()
    D ->> B : requête SQL (psycopg)
    B ->> D : curseur SQL (psycopg)
    D ->> S : instance objet metier
    S ->> R : instance objet metier
    Note over S,R: l'objet est potentiellement altéré
    R ->> U : Réponse HTTP

3.1 Mon premier webservice

    • GET http://localhost/hello
    • GET http://localhost/hello/everybody
    • Cliquez dans le terminal de VSCode puis CTRL + C

En utilisant la liste de personnages définie dans le fichier app.py, ajoutez les endpoints suivants :

Pour tester les endpoints nécessitant un body json, vous pouvez utiliser :

{
  "nom":"Agneta",
  "age": 30
}

Savoir utiliser un webservice est essentiel, car ils représentent une vaste source de données accessibles en temps réel. Les webservices permettent de collecter des informations provenant de multiples sources, comme des APIs publiques ou des systèmes internes, sans avoir besoin de télécharger manuellement des fichiers. À vous ensuite de convertir ces données dans le format que vous préférez.