Semaine 11

This commit is contained in:
gauvainboiche
2026-07-11 21:38:00 +02:00
parent d3d2416dea
commit 0d071d2843
281 changed files with 54412 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
3.14
@@ -0,0 +1,180 @@
# Exercice 3.3 - Jinja2 multi-pages
## Navigation + templates dynamiques (sans base de données)
---
## Objectifs pédagogiques
* structurer une application Flask avec plusieurs routes ;
* générer plusieurs pages HTML avec Jinja2 ;
* utiliser des templates réutilisables ;
* exploiter des données simulées côté serveur (Python) ;
* manipuler boucles et conditions dans Jinja2 ;
* construire une navigation entre pages ;
* séparer clairement logique serveur et affichage.
---
# Contexte
Vous développez une mini application de type **tableau de bord de gestion de parc informatique**.
Les données sont **simulées en mémoire (liste Python)**.
Lapplication doit permettre :
* dafficher une liste globale des équipements ;
* de voir uniquement les équipements actifs ;
* de consulter une page de détail par équipement ;
* de naviguer entre les pages via des liens HTML.
---
# Structure proposée
```
app.py
/templates
base.html
index.html
actifs.html
detail.html
```
---
# Travail demandé
---
## Étape 1 - Initialiser lapplication Flask
Créer une application Flask fonctionnelle.
Lapplication doit pouvoir afficher une page daccueil.
---
## Étape 2 - Créer les données simulées
Définir côté serveur une liste Python représentant des équipements.
Chaque équipement contient :
* un identifiant ;
* un nom ;
* un statut (actif / inactif) ;
* une catégorie (serveur, poste, réseau, etc.).
Ces données sont **fixes et non modifiables**.
---
## Étape 3 - Créer une structure de templates
Mettre en place :
* un template principal servant de base commune ;
* une structure avec bloc de contenu réutilisable ;
* un menu de navigation commun à toutes les pages.
Le menu doit permettre de naviguer entre les pages principales.
---
## Étape 4 - Page daccueil (liste complète)
Créer une route et une page affichant :
* la liste complète des équipements ;
* toutes leurs informations.
Dans le template :
* utiliser une boucle Jinja2 ;
* afficher chaque équipement dans un bloc structuré ;
* distinguer visuellement les équipements actifs et inactifs avec une condition.
---
## Étape 5 - Page “équipements actifs”
Créer une page dédiée affichant uniquement :
* les équipements actifs.
La logique de filtrage doit être réalisée côté serveur (Python), pas dans le template.
Dans le template :
* afficher uniquement les éléments fournis par la vue ;
* utiliser une boucle simple sans logique complexe.
---
## Étape 6 - Page de détail dun équipement
Créer une route dynamique permettant dafficher :
* un équipement spécifique selon son identifiant.
La page doit afficher :
* toutes les informations de l’équipement ;
* un message derreur si lidentifiant nexiste pas.
Dans le template :
* utiliser des variables Jinja2 ;
* utiliser une condition pour gérer le cas “non trouvé”.
---
## Étape 7 - Navigation entre pages
Dans le template principal :
* ajouter un menu de navigation commun ;
* permettre daccéder à toutes les pages ;
* assurer une cohérence visuelle sur toutes les vues.
---
## Étape 8 - Mise en forme Jinja2 obligatoire
Dans les templates, vous devez utiliser :
* des boucles `{% for %}` ;
* des conditions `{% if %}` ;
* des variables `{{ }}` ;
* au moins un bloc réutilisable (`block`) ;
* une logique de filtrage côté Python (obligatoire).
---
## Étape 9
Ajouter :
* une page “statistiques” affichant :
* nombre total d’équipements ;
* nombre dactifs ;
* nombre dinactifs ;
* un message conditionnel si la liste est vide dans une page.
---
# Livrables attendus
* application Flask fonctionnelle ;
* plusieurs routes Python ;
* 3 à 4 templates Jinja2 ;
* aucune base de données ;
* aucune méthode POST ;
* aucune modification de données.
---
# Compétences évaluées
| Compétence | Niveau attendu |
| ---------------------- | ------------------------------- |
| Flask routing | Plusieurs routes fonctionnelles |
| Jinja2 | Boucles + conditions maîtrisées |
| Architecture templates | base.html + héritage |
| Séparation logique/vue | filtrage côté Python |
| Navigation web | cohérence UX simple |
| Lecture de données | affichage structuré |
+61
View File
@@ -0,0 +1,61 @@
# Exo 3.3 "Templates dynamiques" - BOICHÉ Gauvain
Je déclare avec fait usage de l'IA :
# Raison 1
Pour de "l'autocomplétion" avec le plan gratuit de Github Copilot (modèle inconnu) pour générer la base <!DOCTYPE> html parce que ça me casse un peu les noix de refaire ça. C'est un genre d'aamorçage qui devrait exister de base dans les IDE avec un algo à l'ancienne ultra-léger...
## Pourquoi cet usage
Parce que ça me gonfle de refaire ça pour la Xième fois parce que je ne l'ai pas retenu tellement l'HTML me gonfle.
## Ce que j'en retiens
Je dois trouver une extension qui le fait pour moi.
# Raison 2
Comprendre pourquoi mes pages Jinja2 ne "marchent pas".
## Pourquoi cet usage
Refaisant un document <!DOCTYPE> pour chaque page, je n'ai pas su comprendre par les docs accessibles (qui donnaient juste les blocs avec % et variables Python) qu'il fallait les peupler UNIQUEMENT des éléments étendant la "base". Que l'HTML s'oubliait sur ces parties-là. Je prenais les blocs en le mettant dans des balises <body> etc sans succès.
## Ce que j'en ai retenu
Les patrons Jinja2 héritent de TOUT, et on ne rajotue que ce qu'il y a besoin. y compris les bases DOCTYPE.
# Raison 3
## Pourquoi cet usage
Je cliquais sur "actives" et "details" mais je restait avec le contenu de "index". Je demande une revue de code à MiniMax M3, qui me diagnostique trois possibilités :
| Symptom | Likely cause | Remedy |
|---------|--------------|--------|
|Same content on every page | Browser cache serving the old HTML | Hard-reload: Ctrl + F5 (or Cmd + Shift + R on macOS) |
URL changes but body identical | A single route handling all paths | Verify each @app.route is distinct |
Template not overridden | Missing {% block content %} in child | Ensure children declare the block |
J'essaye, et rien.
## Ce que j'en ai retenu
Que je suis un imbécile. Je demande à l'IA de m'aider, et rien n'y a fait. Jusqu'à ce que je vois ma connerie :
```
@app.route('/')
def index():
return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS)
@app.route('/actives')
def actives():
return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS)
@app.route('/details')
def details():
return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS)
```
Trois fois "index.html". Bon. Résultat, je corrige et ça marche. L'IA ne m'a servi à RIEN ici, sauf ceci : des fois, il faut aller faire un tour, boire de l'eau, revenir et relire.
+54
View File
@@ -0,0 +1,54 @@
from flask import Flask, render_template, abort
app = Flask(__name__)
EQUIPMENTS = [
{"id": 1, "hostname": "srv-web-main-01", "type": "server", "active": True},
{"id": 2, "hostname": "sw-access-01", "type": "switch", "active": True},
{"id": 3, "hostname": "srv-api-01", "type": "server", "active": True},
{"id": 4, "hostname": "waf-front-01", "type": "waf", "active": True},
{"id": 5, "hostname": "individual-alice-01", "type": "instance", "active": True},
{"id": 6, "hostname": "sw-access-02", "type": "switch", "active": False},
{"id": 7, "hostname": "individual-bob-01", "type": "instance", "active": False},
{"id": 8, "hostname": "srv-web-replica-01", "type": "server", "active": True},
]
USERNAME = "administratort"
@app.route('/')
def index():
return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS)
@app.route('/actives')
def actives():
ACTIVE_EQUIPMENTS = [e for e in EQUIPMENTS if e["active"]]
return render_template('actives.html', username=USERNAME, equipments=ACTIVE_EQUIPMENTS)
@app.route('/list')
def details():
return render_template('list.html', username=USERNAME, equipments=EQUIPMENTS)
@app.route("/details/<int:equipment_id>")
def equipment_detail(equipment_id):
equipment = next((e for e in EQUIPMENTS if e["id"] == equipment_id), None)
return render_template(
"details.html",
username=USERNAME,
equipment=equipment,
)
@app.route("/stats")
def stats():
total = len(EQUIPMENTS)
active = sum(1 for e in EQUIPMENTS if e["active"])
inactive = total - active
return render_template(
"stats.html",
username=USERNAME,
total=total,
active=active,
inactive=inactive,
)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)
@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Active devices - Network Inventory{% endblock %}
{% block content %}
<h1>Active devices</h1>
<p>Signed-in as <strong>{{ username }}</strong>.</p>
<ul>
{% for equipment in equipments %}
<li>{{ equipment.hostname }}</li>
{% else %}
<li><em>No active device.</em></li>
{% endfor %}
</ul>
{% endblock %}
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}NETWORK INVENTORY{% endblock %}</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
header, footer { background: #f4f4f4; padding: 1rem; }
nav a { margin-right: 1rem; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: left; }
th { background: #eee; }
.up { color: green; font-weight: bold; }
.down { color: red; font-weight: bold; }
</style>
</head>
<body>
<header>
<nav>
<strong>- MENU -</strong>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('actives') }}">Active devices</a>
<a href="{{ url_for('details') }}">Details</a>
<a href="{{ url_for('stats') }}">Stats</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>Gauvain B. - Flask Jinja2 app</footer>
</body>
</html>
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Equipment - Network Inventory{% endblock %}
{% block content %}
<p>Signed-in as <strong>{{ username }}</strong>.</p>
{% if equipment %}
<h1>{{ equipment.hostname }}</h1>
<table>
<tbody>
<tr>
<th>ID</th>
<td>{{ equipment.id }}</td>
</tr>
<tr>
<th>Hostname</th>
<td>{{ equipment.hostname }}</td>
</tr>
<tr>
<th>Type</th>
<td>{{ equipment.type }}</td>
</tr>
<tr>
<th>State</th>
<td>
{% if equipment.active %}
<span class="up">UP</span>
{% else %}
<span class="down">DOWN</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<h1>There's no device with this ID</h1>
<p>The requested equipment (ID {{ equipment_id }}) does not exist.</p>
{% endif %}
<p><a href="{{ url_for('details') }}">← Back to all devices</a></p>
{% endblock %}
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Home - Network Inventory{% endblock %}
{% block content %}
<h1>Welcome, {{ username }}.</h1>
{% if username == "administratort" %}
<p>You signed-in with ADMIN rights.</p>
{% else %}
<p>You signed-in with USER rights.</p>
{% endif %}
<h2>Devices list:</h2>
<ul>
{% for equipment in equipments %}
<li>{{ equipment.hostname }}</li>
{% endfor %}
</ul>
{% endblock %}
@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Details - Network Inventory{% endblock %}
{% block content %}
<h1>Device details</h1>
<p>Signed-in as <strong>{{ username }}</strong>.</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Hostname</th>
<th>Type</th>
<th>State</th>
</tr>
</thead>
<tbody>
{% for equipment in equipments %}
<tr>
<td>{{ equipment.id }}</td>
<td>
<a href="{{ url_for('equipment_detail', equipment_id=equipment.id) }}">
{{ equipment.hostname }}
</a>
</td>
<td>{{ equipment.type }}</td>
<td>
{% if equipment.active %}
<span class="up">UP</span>
{% else %}
<span class="down">DOWN</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Statistics - Network Inventory{% endblock %}
{% block content %}
<h1>Inventory statistics</h1>
<p>Signed-in as <strong>{{ username }}</strong>.</p>
{% if total > 0 %}
<table>
<thead>
<tr>
<th>Metric</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total equipments</td>
<td>{{ total }}</td>
</tr>
<tr>
<td>Active</td>
<td><span class="up">{{ active }}</span></td>
</tr>
<tr>
<td>Inactive</td>
<td><span class="down">{{ inactive }}</span></td>
</tr>
</tbody>
</table>
{% else %}
<p><em>There's no equipment in the inventory.</em></p>
{% endif %}
{% endblock %}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
# Module 3 — Exercices : Flask & Requests
Fil rouge : une **API d'inventaire** en Flask, puis sa **consommation** avec la bibliothèque `requests`.
---
## Consignes communes à tous les exercices
**Usage de l'IA — transparence obligatoire.**
L'IA générative n'est **pas interdite**, mais son usage doit être **assumé et documenté**. Chaque rendu **doit obligatoirement contenir un fichier `USAGE-IA.md`**. Sans ce fichier, le rendu est **incomplet**.
`USAGE-IA.md` doit préciser : (1) les **outils utilisés** et pour quelles parties, (2) l'**aide obtenue** (avec un exemple de prompt), (3) ce que vous avez **accepté** et *pourquoi vous l'avez compris/validé*, (4) ce que vous avez **refusé ou corrigé** et *pourquoi*, (5) ce que vous avez **appris**.
> Aucune IA utilisée ? Déclarez-le explicitement.
**Règle d'or : vous devez pouvoir expliquer et justifier chaque ligne de votre rendu.** Gabarit en annexe.
**Pré-requis :** environnement virtuel activé, `pip install flask requests`.
---
## Exercice 3.1 — Première API Flask en lecture
**Compétences visées :** routing Flask, réponses JSON (`jsonify`), code **404**.
**Consignes :**
1. Créez une application Flask avec une liste d'équipements en mémoire (`id`, `hostname`, `ip`).
2. `GET /api/equipements` → renvoie `{"equipements": [...]}` en JSON.
3. `GET /api/equipements/<int:equipement_id>` → renvoie l'équipement, ou `{"erreur": "..."}` avec le code **404** s'il n'existe pas.
4. Lancez le serveur et testez dans le navigateur ou avec `curl`.
**Livrables :** `app.py`, `USAGE-IA.md`.
**Critères d'évaluation :** routes correctes (statique + dynamique), sérialisation JSON, gestion du 404.
## Annexe — Gabarit `USAGE-IA.md`
```markdown
# Usage de l'IA — Exercice X.Y
## 1. Outils utilisés
(Quel(s) outil(s), pour quelles parties. Ou : « aucune IA utilisée ».)
## 2. Aide obtenue
(Ce que l'IA a proposé. Exemple de prompt : « … »)
## 3. Ce que j'ai accepté
(Ce que j'ai repris et pourquoi je l'ai compris et validé.)
## 4. Ce que j'ai refusé ou corrigé
(Ce que j'ai écarté / modifié, et pourquoi.)
## 5. Ce que j'ai appris
(Ce que je retiens, ce que je ferais autrement.)
```
@@ -0,0 +1,15 @@
## Exercice 3.2 — API CRUD Flask complète
**Compétences visées :** `POST` / `PUT` / `DELETE`, lecture du corps JSON (`request.get_json`), **validation**, codes **201 / 400 / 404**.
**Consignes :** repartez de l'API du 3.1 et ajoutez :
1. `POST /api/equipements` : crée un équipement depuis le JSON reçu. `hostname` est **obligatoire** (sinon **400**). Renvoie **201** et l'objet créé (avec un `id` auto-incrémenté).
2. `PUT /api/equipements/<id>` : met à jour les champs fournis ; **404** si l'équipement n'existe pas.
3. `DELETE /api/equipements/<id>` : supprime l'équipement ; **404** si absent.
4. Vérifiez chaque opération avec `curl`.
**Livrables :** `app.py`, `TESTS.md` (vos commandes `curl` et les réponses), `USAGE-IA.md`.
**Critères d'évaluation :** CRUD complet et cohérent, validation des entrées, bons codes HTTP.
---
View File
+181
View File
@@ -0,0 +1,181 @@
# Exo 3.2 "API CRUD" - BOICHÉ Gauvain
## Commandes
### `POST /api/equipements`
#### Côté client
```
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname": "test-web-35"}'
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:48:45 GMT
Content-Type: application/json
Content-Length: 72
Connection: close
{
"actif": true,
"hostname": "test-web-35",
"id": 3,
"ip": ""
}
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname": "test-web-37", "ip": "192.168.250.69"}'
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:49:10 GMT
Content-Type: application/json
Content-Length: 86
Connection: close
{
"actif": true,
"hostname": "test-web-37",
"id": 4,
"ip": "192.168.250.69"
}
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"ip": "192.168.250.67"}'
HTTP/1.1 400 BAD REQUEST
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:49:20 GMT
Content-Type: application/json
Content-Length: 46
Connection: close
{
"erreur": "Le hostname est obligatoire"
}
```
#### Côté serveur
```
127.0.0.1 - - [07/Jul/2026 11:48:45] "POST /api/equipments HTTP/1.1" 201 -
127.0.0.1 - - [07/Jul/2026 11:49:10] "POST /api/equipments HTTP/1.1" 201 -
127.0.0.1 - - [07/Jul/2026 11:49:20] "POST /api/equipments HTTP/1.1" 400 -
```
### `PUT /api/equipements/<id>`
#### Côté client
```
> curl -i http://127.0.0.1:5000/api/equipments
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:50:19 GMT
Content-Type: application/json
Content-Length: 452
Connection: close
{
"equipments": [
{
"active": true,
"hostname": "srv-web-01",
"id": 1,
"ip": "192.168.1.10"
},
{
"active": true,
"hostname": "sw-access-02",
"id": 2,
"ip": "192.168.1.2"
},
{
"actif": true,
"hostname": "test-web-35",
"id": 3,
"ip": ""
},
{
"actif": true,
"hostname": "test-web-37",
"id": 4,
"ip": "192.168.250.69"
}
]
}
> curl -i -X PUT http://127.0.0.1:5000/api/equipments/4 -H "Content-Type: application/json" -d '{"ip": "192.168.4.20"}'
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:50:56 GMT
Content-Type: application/json
Content-Length: 84
Connection: close
{
"actif": true,
"hostname": "test-web-37",
"id": 4,
"ip": "192.168.4.20"
}
> curl -i -X PUT http://127.0.0.1:5000/api/equipments/5 -H "Content-Type: application/json" -d '{"ip": "192.168.39.45"}'
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:51:08 GMT
Content-Type: application/json
Content-Length: 50
Connection: close
{
"erreur": "\u00c9quipement non trouv\u00e9"
}
```
#### Côté serveur
```
127.0.0.1 - - [07/Jul/2026 11:50:19] "GET /api/equipments HTTP/1.1" 200 -
127.0.0.1 - - [07/Jul/2026 11:50:56] "PUT /api/equipments/4 HTTP/1.1" 200 -
127.0.0.1 - - [07/Jul/2026 11:51:08] "PUT /api/equipments/5 HTTP/1.1" 404 -
```
### `DELETE /api/equipements/<id>`
#### Côté client
```
> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/5
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:52:51 GMT
Content-Type: application/json
Content-Length: 50
Connection: close
{
"erreur": "\u00c9quipement non trouv\u00e9"
}
> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/4
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:52:54 GMT
Content-Type: application/json
Content-Length: 66
Connection: close
{
"message": "\u00c9quipement supprim\u00e9 avec succ\u00e8s"
}
> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/4
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 09:52:57 GMT
Content-Type: application/json
Content-Length: 50
Connection: close
{
"erreur": "\u00c9quipement non trouv\u00e9"
}
```
#### Côté serveur
```
127.0.0.1 - - [07/Jul/2026 11:52:51] "DELETE /api/equipments/5 HTTP/1.1" 404 -
127.0.0.1 - - [07/Jul/2026 11:52:54] "DELETE /api/equipments/4 HTTP/1.1" 200 -
127.0.0.1 - - [07/Jul/2026 11:52:57] "DELETE /api/equipments/4 HTTP/1.1" 404 -
```
+96
View File
@@ -0,0 +1,96 @@
# Exo 3.1 "API Flask" - BOICHÉ Gauvain
Je déclare n'avoir pas usé de l'IA. J'ai en revanche utilisé à foison la documentation du cours :
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-3-Installation-de-Flask-et-cr%C3%A9ation-d.md
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-4-Routing%2C-vues%2C-r%C3%A9ponses-HTTP-(JSON).md
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-5-Cr%C3%A9ation-d'APIs-web-(GET%2C-POST%2C-PUT.md
Et la seule erreur que j'avais eu n'a pas nécessité d'IA non plus.
Ligne 34 : `@app.route('/api/equipments/<int:equipment_id>', methods=['GET'])`
> renvoyait une erreur type "TraceError callback" parce que le contenu était `@app.route('/api/equipments/<int:id_equipement>', methods=['GET'])`. Une harmonisation des noms de variables a fait l'affaire.
# Résultats
## Côté client
```
> curl -i http://127.0.0.1:5000/api/equipments
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 08:40:08 GMT
Content-Type: application/json
Content-Length: 244
Connection: close
{
"equipments": [
{
"active": true,
"hostname": "srv-web-01",
"id": 1,
"ip": "192.168.1.10"
},
{
"active": true,
"hostname": "sw-access-02",
"id": 2,
"ip": "192.168.1.2"
}
]
}
> curl -i http://127.0.0.1:5000/api/equipments/2
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 08:40:10 GMT
Content-Type: application/json
Content-Length: 85
Connection: close
{
"active": true,
"hostname": "sw-access-02",
"id": 2,
"ip": "192.168.1.2"
}
> curl -i http://127.0.0.1:5000/api/equipments/25
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.1.8 Python/3.14.6
Date: Tue, 07 Jul 2026 08:43:01 GMT
Content-Type: application/json
Content-Length: 50
Connection: close
{
"erreur": "\u00c9quipement non trouv\u00e9"
}
(jour-02) PS D:\Users\GaWin\LiveCampus\Semaine_11\Jour_02>
```
## Côté serveur
```
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 917-152-107
127.0.0.1 - - [07/Jul/2026 10:40:08] "GET /api/equipments HTTP/1.1" 200 -
127.0.0.1 - - [07/Jul/2026 10:40:10] "GET /api/equipments/2 HTTP/1.1" 200 -
* Detected change in '.\app.py', reloading
* Restarting with stat
* Debugger is active!
* Debugger PIN: 917-152-107
127.0.0.1 - - [07/Jul/2026 10:43:01] "GET /api/equipments/25 HTTP/1.1" 404 -
```
# Exo 3.2 "API CRUD" - BOICHÉ Gauvain
Je déclare n'avoir pas usé de l'IA là non plus. Toujours utilisé à foison la documentation du cours :
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-3-Installation-de-Flask-et-cr%C3%A9ation-d.md
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-4-Routing%2C-vues%2C-r%C3%A9ponses-HTTP-(JSON).md
- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-5-Cr%C3%A9ation-d'APIs-web-(GET%2C-POST%2C-PUT.md
+103
View File
@@ -0,0 +1,103 @@
"""
**Consignes 3_1 :**
1. Créez une application Flask avec une liste d'équipements en mémoire (`id`, `hostname`, `ip`).
2. `GET /api/equipments` → renvoie `{"equipments": [...]}` en JSON.
3. `GET /api/equipments/<int:equipment_id>` → renvoie l'équipement, ou `{"erreur": "..."}` avec le code **404** s'il n'existe pas.
4. Lancez le serveur et testez dans le navigateur ou avec `curl`.
**Consignes 3_2 :** repartez de l'API du 3.1 et ajoutez :
1. `POST /api/equipments` : crée un équipement depuis le JSON reçu. `hostname` est **obligatoire** (sinon **400**). Renvoie **201** et l'objet créé (avec un `id` auto-incrémenté).
2. `PUT /api/equipments/<id>` : met à jour les champs fournis ; **404** si l'équipement n'existe pas.
3. `DELETE /api/equipments/<id>` : supprime l'équipement ; **404** si absent.
4. Vérifiez chaque opération avec `curl`.
"""
from flask import Flask, request, jsonify
app = Flask(__name__)
equipments = [
{"id": 1, "hostname": "srv-web-01", "ip": "192.168.1.10", "active": True},
{"id": 2, "hostname": "sw-access-02", "ip": "192.168.1.2", "active": True}
]
@app.route('/api/statut', methods=['GET'])
def statut_api():
return jsonify({
"statut": "succès",
"message": "L'API d'inventaire réseau fonctionne correctement",
"version": "1.0"
}), 200
@app.route('/api/equipments', methods=['GET'])
def list_equipments():
return jsonify({"equipments": equipments}), 200
@app.route('/api/equipments/<int:equipment_id>', methods=['GET'])
def get_equipment(equipment_id):
equipment = next((e for e in equipments if e["id"] == equipment_id), None)
if equipment is None:
return jsonify({"erreur": "Équipement non trouvé"}), 404
return jsonify(equipment), 200
@app.route('/api/equipments', methods=['POST'])
def post_equipment():
data = request.get_json()
if not data or not 'hostname' in data:
return jsonify({"erreur": "Le hostname est obligatoire"}), 400
new_equipment = {
"id": max(e['id'] for e in equipments) + 1 if equipments else 1,
"hostname": data['hostname'],
"ip": data.get('ip', ""),
"actif": True
}
equipments.append(new_equipment)
return jsonify(new_equipment), 201
@app.route('/api/equipments/<int:equipment_id>', methods=['PUT'])
def put_equipment(equipment_id):
equipment = next((e for e in equipments if e["id"] == equipment_id), None)
if equipment is None:
return jsonify({"erreur": "Équipement non trouvé"}), 404
donnees = request.get_json()
if 'hostname' in donnees:
equipment['hostname'] = donnees['hostname']
if 'ip' in donnees:
equipment['ip'] = donnees['ip']
if 'actif' in donnees:
equipment['actif'] = donnees['actif']
return jsonify(equipment), 200
@app.route('/api/equipments/<int:equipment_id>', methods=['DELETE'])
def delete_equipment(equipment_id):
global equipments
equipment = next((e for e in equipments if e["id"] == equipment_id), None)
if equipment is None:
return jsonify({"erreur": "Équipement non trouvé"}), 404
equipments = [e for e in equipments if e["id"] != equipment_id]
return jsonify({"message": "Équipement supprimé avec succès"}), 200
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)
+6
View File
@@ -0,0 +1,6 @@
def main():
print("Hello from jour-02!")
if __name__ == "__main__":
main()
+9
View File
@@ -0,0 +1,9 @@
[project]
name = "jour-02"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"flask>=3.1.3",
]
+124
View File
@@ -0,0 +1,124 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "click"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "flask"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jour-02"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "flask" },
]
[package.metadata]
requires-dist = [{ name = "flask", specifier = ">=3.1.3" }]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
]