Semaine 11
This commit is contained in:
@@ -0,0 +1 @@
|
||||
3.14
|
||||
Binary file not shown.
@@ -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)**.
|
||||
|
||||
L’application doit permettre :
|
||||
|
||||
* d’afficher 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 l’application Flask
|
||||
|
||||
Créer une application Flask fonctionnelle.
|
||||
L’application doit pouvoir afficher une page d’accueil.
|
||||
|
||||
---
|
||||
|
||||
## É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 d’accueil (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 d’un équipement
|
||||
|
||||
Créer une route dynamique permettant d’afficher :
|
||||
* un équipement spécifique selon son identifiant.
|
||||
|
||||
La page doit afficher :
|
||||
* toutes les informations de l’équipement ;
|
||||
* un message d’erreur si l’identifiant n’existe 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 d’accé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 d’actifs ;
|
||||
* nombre d’inactifs ;
|
||||
* 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é |
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
@@ -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 -
|
||||
```
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from jour-02!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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",
|
||||
]
|
||||
Generated
+124
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user