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
Webservices et formats de données
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.
❗ Il est possible que les copier-coller fonctionnent étrangement (caractère de fin de ligne qui disparaissent, indentation qui change). Faites-y attention !
Ce TP mêle explications pour vous faire comprendre ce qui est fait, et phases de manipulation ou code. Ces phases sont appelées “✍️Hands on”. C’est à ce moment là que vous devez faire ce qui est écrit dans le TP. Les explications de ce TP ne doivent pas prendre le pas sur celles de votre intervenant. Prenez les comme une base de connaissances pour plus tard, mais préférez toujours les explications orales, surtout pour poser des questions.
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 Insomnia.
1.1 Webservices
📖 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, soit 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’information (lancer des travaux, les mettre en attente, récupérer des résultats, etc)
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, ou URI (c’est une notion plus générale que les Uniforme Resource Locator, ou URL), une méthode (GET, POST, PUT, DELETE, liste des méthodes), et potentiellement des données.
1.2 Découverte d’Insomnia et premières requêtes GET
✍️Hands on 1
-
- 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
-
- Regardez la réponse dans la partie droite de votre écran.
- Quelles sont les similarités entre les réponses ?
Requêtes à tester :
- Webservice sur les émissions carbone du Royaume-Uni :
api.carbonintensity.org.uk/intensity
api.carbonintensity.org.uk/intensity/date/{date}
- en remplaçant {date} par la date de votre choix au format YYYY-MM-DD
- Webservice pour obtenir différents jeux de données ouverts de la ville de Rennes
data.rennesmetropole.fr/api/records/1.0/search?dataset=menus-cantines
- Testez différentes valeurs pour dataset :
eco-counter-data
,rva-bal
,resultats-des-elections-municipales-2020-a-acigne
- Ajouter à la fin de l’URI le paramètre
rows
- pour faire varier le nombre de lignes que vous recevez
- ajouter simplement
&rows=X
avec X le nombre de lignes
- Quelques méthodes du webservice utiles pour votre projet informatique (voyez cela avec votre tuteur)
1.3 Requêtes avancées
✍️Hands on 2 (toujours avec Insomnia)
- Faites une requête avec la méthode
GET
sur la ressource suivante. Qu’obtenez-vous ?web-services.domensai.ecole/attack
- Faites une requête avec la méthode
GET
sur la ressource suivante. Qu’obtenez-vous ?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
- Faites une requête avec la méthode
GET
sur la ressource suivante. Qu’obtenez-vous ?web-services.domensai.ecole/attack?type_attack_id={id_type}
- en remplaçant
{id_type}
par un entier entre 1 et 4.
- Faites une requête avec la méthode
GET
sur la ressource suivanteweb-services.domensai.ecole/attack?type_attack_name={type attack}
- en remplaçant
{type attack}
parspecial attack
ouphysical attack
oufixed damage
oustatus attack
- Faites une requête de type
POST
sur la ressource suivanteweb-services.domensai.ecole/attack
- Cliquer 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" }
- Faites une requête avec la méthode
GET
sur la ressource suivanteweb-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 Appeler un webservice en python
Aujourd’hui, les plus grands consommateurs de webservices sont les machines. Et donc maintenant nous allons voir comment automatiser des appels à un webservice en python.
🔍 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 leurs 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 requête 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.
Le côté négatif c’est que 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
- Comment ça fonctionne
Le principe va rester le même que faire une requête à la main, et on va utiliser la bibliothèque requests pour avoir seulement à remplir les parties intéressantes de nos requêtes.
Pour faire une requête GET
vous allez seulement devoir faire :
import requests
= requests.get("http://mon-webservice.com") response
Exécuter cette ligne de code va :
- Envoyer la requête au serveur que vous contactez
- 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 laissantrequests
inférer l’encodage (cela fonctionne souvent). Problème vous avez seulement un string, et ce n’est pas le meilleur format de données à manipulerresponse.json()
: le corps du résultat comme undict
. C’est ce que vous allez faire le plus souvent car le format json est un format simple à manipulerresponse.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.
Exemple simple d’utilisation :
import requests
import json
= requests.get("http://mon-webservice.com")
response
if response.status_code != 200:
raise Exception(
"Cannot reach (HTTP {}): {}".format(response.status_code, response.text)
)else:
print(json.dumps(response.json(), indent=2)) # JSON Pretty print
2.2 Mise à jour de votre dépôt git
2 possibilités au choix
2.2.1 Si vous voulez repartir du code du TP1
- Ouvrez Visual Studio Code
- File > Open Folder
- Allez dans
/p/Cours2A/UE3_Complements_informatique/TP/TP1
- cliquez une fois sur ENSAI-2A-complement-info-TP
- puis sur le bouton Sélectionner un dossier
- Allez dans
- Ouvrez un Terminal Git Bash dans VSCode (Terminal > New terminal)
- Créez un point de sauvegarde de vos travaux de la semaine dernière
git add .
git commit -m "Mon super code du TP1"
- Mettez à jour votre dépôt local
git pull
- Passez sur la branche du TP2
git checkout tp2_base
- File > Open Folder
2.2.3 ⚠️ Attention quand vous faites Open Folder dans VSCode
Le dossier parent de l’explorer de VSCode (à gauche) doit être : ENSAI-2A-complement-info-TP. Si c’est TP1, TP2, TP ou autre chose ce n’est pas bon ! Vous allez avoir des soucis d’imports par la suite.
Pour pour vérifier que tout fonctionne : * lancez le fichier __main__.py
* lancez les tests unitaires du package business_object * dans terminal : python -m unittest -k test_business_object
2.3 Mes premières requêtes en Python
✍️ Hands on 3
- Si ce n’est pas déjà fait (voir README.md), installez dotenv
pip install python-dotenv
- Ouvrez le fichier
/src/client/attack_client.py
-
- prend en paramètre un id d’attaque
- va chercher toutes les informations disponibles sur cette attaque
- retourne un objet de type
AbstractAttack
- Pour vous aider, observez la méthode
instantiate_attack()
de la classeAttackFactory
- Regardez le fonctionnement de cette méthode et utilisez la
- Pour vous aider, observez la méthode
-
- retourne la liste de tous les attaques disponibles sous la forme d’une liste d’objets
AbstractAttack
- retourne la liste de tous les attaques disponibles sous la forme d’une liste d’objets
-
- Lancez les tests unitaitres du package test_client
2.4 Les requêtes plus complexes
Pour le moment nous nous sommes concentrés sur les requêtes GET
mais il est bien sûr possible d’en faire d’autre. Par exemple pour les requêtes POST
, PUT
ou DELETE
voici la syntaxe :
= requests.post("http://example.org", json = {'key':'value'})
post = requests.put("http://example.org", json = {'key':'value'})
put = requests.delete("http://example.org") delete
Comme vous le voyez, les syntaxes sont très proches de la syntaxe de la méthode GET
. On a 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 :
= "http://example.org"
url = {'key':'value'}
data = requests.post(url, json = data) post
C’est la même chose fonctionnellement, 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 l’attribut headers
à la fonction utilisée.
= {'accept': 'application/xml'}
headers 'http://example.org', headers=headers) requests.get(
2.5 Requêtes avancées en python
✍️ Hands on 4
- 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
-
- prend une
AbstractAttack
en paramètre - modifie la ressource associée dans notre webservice
- prend une
-
- prend une
AbstractAttack
en paramètre - supprime la ressource associée dans notre webservice
- prend une
-
3 Coder un webservice en python
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: * Django REST, * FlaskRESTful * FastAPI
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
= FastAPI()
app
# 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__":
="0.0.0.0", port=80) uvicorn.run(app, host
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
= FastAPI()
app
class Todo(BaseModel):
id : int
str
content :
= {1 : Todo(1,"Step 1 : Learn python")
todos 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 :
id] = todo
todos[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__":
="0.0.0.0", port=8XXX) uvicorn.run(app, host
Ce code va créer un web service qui va répondre aux requêtes suivantes :
GET host/todo
: retourne toutes les tâches à faireGET host/todo/{todo_id}
: retourne la tâche derrière l’id en paramètrePOST 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
str content :
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ôle 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 si je reprends le modèle 3 couches vu en cours.
3.1 Mon premier webservice
- Vérifiez que le module
fastapi
est installé (pip list
)- Si ce n’est pas le cas :
pip install "fastapi[all]"
- Si ce n’est pas le cas :
- Ouvrez le fichier
app.py
- Lancez ce fichier
- testez les requêtes suivantes :
GET http://localhost/hello
GET http://localhost/hello/everybody
- testez les requêtes suivantes :
- Arrétez le webservice
- Cliquez dans le terminal de VSCode puis CTRL + C
✍️ Hands on 5
En utilsant la liste de personnages définie dans le fichier app.py
, ajoutez les endpoints suivants : * [ ] GET localhost:80/character
: retournera un json contenant une liste des personnages * [ ] PUT localhost:80/character/{id}
qui modifiera le nom du personnage à l’index {id}
à partir d’un body * [ ] DELETE localhost:80/character/{id}
qui supprimera l’élément à l’index {id}
Pour tester les endpoints nécessitant un body json, vous pouvez utiliser 👎
{
"nom":"Agneta",
"age": 30
}