Semaine 11
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Clé secrète Flask : utilisée pour signer les sessions et flash messages.
|
||||
# Générer avec : python -c "import secrets; print(secrets.token_hex(32))"
|
||||
FLASK_SECRET_KEY=hex(32)
|
||||
|
||||
# Mode debug (True/False). Désactiver en production.
|
||||
FLASK_DEBUG=False
|
||||
|
||||
# Hôte et port d'écoute Flask
|
||||
FLASK_HOST=127.0.0.1
|
||||
FLASK_PORT=5000
|
||||
|
||||
# Chemin optionnel vers la base SQLite (relatif au projet)
|
||||
DATABASE_PATH=databases/inventory.db
|
||||
@@ -0,0 +1,40 @@
|
||||
# --- Secrets ---
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# --- Python ---
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
.eggs/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# --- Base de données locale ---
|
||||
databases/*.db
|
||||
databases/*.sqlite
|
||||
databases/*.sqlite3
|
||||
|
||||
# --- IDE / OS ---
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# --- Logs ---
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# --- Tests / coverage ---
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
@@ -0,0 +1 @@
|
||||
3.14
|
||||
Binary file not shown.
@@ -0,0 +1,204 @@
|
||||
# Exercice 3.4 — Développer une application Flask avec API REST, interface Jinja2 et base SQLite3
|
||||
|
||||
## Objectifs pédagogiques
|
||||
|
||||
À l'issue de cet exercice, vous serez capable de :
|
||||
|
||||
* créer une application Flask combinant une API REST et une interface Web ;
|
||||
* utiliser une base de données SQLite3 pour stocker des informations ;
|
||||
* réaliser les opérations essentielles d'un CRUD (Create, Read, Update, Delete) ;
|
||||
* concevoir une interface HTML dynamique avec Jinja2 ;
|
||||
* créer un formulaire HTML permettant d'ajouter des données ;
|
||||
* consommer une API REST avec la bibliothèque `requests` ;
|
||||
* gérer correctement les erreurs de communication avec une API.
|
||||
|
||||
---
|
||||
|
||||
# Contexte
|
||||
|
||||
Une entreprise souhaite développer une petite application permettant de gérer son parc d'équipements informatiques.
|
||||
|
||||
L'application devra être accessible de deux façons :
|
||||
|
||||
* par une API REST destinée à d'autres applications ;
|
||||
* par une interface Web destinée aux utilisateurs.
|
||||
|
||||
Toutes les données devront être enregistrées dans une base de données SQLite3.
|
||||
|
||||
---
|
||||
|
||||
# Travail demandé
|
||||
|
||||
## Étape 1 — Préparer le projet
|
||||
|
||||
* Créez un nouveau projet Python et mettez en place un environnement virtuel.
|
||||
* Installez les bibliothèques nécessaires au développement de l'application.
|
||||
* Organisez votre projet de manière à séparer le code Python, les templates HTML et la base de données.
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — Créer la base de données
|
||||
|
||||
Créez une base SQLite3.
|
||||
Cette base devra contenir une table permettant de stocker les équipements.
|
||||
Chaque équipement devra posséder au minimum :
|
||||
|
||||
* un identifiant unique ;
|
||||
* un nom ;
|
||||
* un état (actif ou inactif).
|
||||
|
||||
Vérifiez que la table est correctement créée.
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — Initialiser Flask
|
||||
|
||||
Créez une application Flask.
|
||||
Assurez-vous que celle-ci démarre correctement et qu'une première page de test est accessible depuis un navigateur.
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — Accéder à la base de données
|
||||
|
||||
Écrivez les fonctions permettant :
|
||||
|
||||
* d'ouvrir une connexion à SQLite ;
|
||||
* d'exécuter une requête SQL ;
|
||||
* de récupérer les résultats ;
|
||||
* de fermer correctement la connexion.
|
||||
|
||||
Ces fonctions devront être réutilisées dans toute l'application.
|
||||
|
||||
---
|
||||
|
||||
## Étape 5 — Développer l'API REST
|
||||
|
||||
Créez les routes permettant d'exposer les données sous forme JSON.
|
||||
|
||||
Votre API devra permettre :
|
||||
|
||||
* d'obtenir la liste complète des équipements ;
|
||||
* d'ajouter un nouvel équipement.
|
||||
|
||||
Vérifiez le bon fonctionnement de ces routes avec un navigateur ou un outil de test d'API.
|
||||
|
||||
---
|
||||
|
||||
## Étape 6 — Réaliser l'interface Web
|
||||
|
||||
Ajoutez une nouvelle page Web affichant le parc des équipements.
|
||||
Cette page devra être générée avec un template Jinja2.
|
||||
Les données affichées devront provenir de la base SQLite.
|
||||
|
||||
---
|
||||
|
||||
## Étape 7 — Construire le template Jinja2
|
||||
|
||||
Dans votre template :
|
||||
|
||||
* affichez les équipements dans un tableau HTML ;
|
||||
* utilisez une boucle Jinja2 pour parcourir les données ;
|
||||
* affichez le nom et l'identifiant de chaque équipement ;
|
||||
* affichez l'état de chaque équipement.
|
||||
|
||||
L'état devra être affiché différemment selon que l'équipement est actif ou inactif.
|
||||
|
||||
---
|
||||
|
||||
## Étape 8 — Ajouter un formulaire
|
||||
|
||||
Ajoutez au-dessus du tableau un formulaire HTML permettant de créer un nouvel équipement.
|
||||
|
||||
Le formulaire devra comporter :
|
||||
|
||||
* un champ de saisie pour le nom ;
|
||||
* un bouton d'envoi.
|
||||
|
||||
Lors de la validation :
|
||||
|
||||
* les données devront être envoyées au serveur ;
|
||||
* le nouvel équipement devra être enregistré dans SQLite ;
|
||||
* la page devra être réaffichée avec la nouvelle liste.
|
||||
|
||||
---
|
||||
|
||||
## Étape 9 — Ajouter des actions sur les équipements
|
||||
|
||||
Pour chaque ligne du tableau, ajoutez des boutons permettant :
|
||||
|
||||
* de modifier l'état de l'équipement (actif ↔ inactif) ;
|
||||
* de supprimer l'équipement.
|
||||
|
||||
Après chaque action, l'utilisateur devra être redirigé vers la liste mise à jour.
|
||||
|
||||
---
|
||||
|
||||
## Étape 10 — Développer un client Python
|
||||
|
||||
Créez un second programme Python jouant le rôle de client.
|
||||
|
||||
Ce programme devra communiquer avec l'API REST à l'aide de la bibliothèque `requests`.
|
||||
|
||||
Il devra :
|
||||
|
||||
1. récupérer la liste des équipements ;
|
||||
2. afficher cette liste dans le terminal ;
|
||||
3. ajouter un nouvel équipement via l'API ;
|
||||
4. récupérer de nouveau la liste ;
|
||||
5. afficher le résultat.
|
||||
|
||||
---
|
||||
|
||||
## Étape 11 — Gérer les erreurs
|
||||
|
||||
Tous les appels HTTP devront être sécurisés.
|
||||
|
||||
Votre programme devra notamment :
|
||||
|
||||
* définir un délai maximal d'attente pour chaque requête ;
|
||||
* vérifier que la réponse du serveur est valide ;
|
||||
* afficher un message clair si le serveur est indisponible ;
|
||||
* éviter tout arrêt brutal du programme en cas d'erreur réseau.
|
||||
|
||||
---
|
||||
|
||||
## Étape 12 — Vérifier le fonctionnement
|
||||
|
||||
Vérifiez que les opérations suivantes fonctionnent correctement :
|
||||
|
||||
* création de la base de données ;
|
||||
* ajout d'un équipement via le formulaire Web ;
|
||||
* affichage dynamique des données dans le template Jinja2 ;
|
||||
* modification de l'état d'un équipement ;
|
||||
* suppression d'un équipement ;
|
||||
* consultation de l'API REST ;
|
||||
* ajout d'un équipement via le client Python.
|
||||
|
||||
---
|
||||
|
||||
# Livrables attendus
|
||||
|
||||
Le projet devra contenir au minimum :
|
||||
|
||||
* le fichier principal de l'application Flask ;
|
||||
* le client Python utilisant `requests` ;
|
||||
* la base SQLite3 (ou son script de création) ;
|
||||
* le dossier `templates` contenant les pages Jinja2 ;
|
||||
* un document `USAGE-IA.md` décrivant l'utilisation éventuelle d'une IA générative pendant le développement.
|
||||
|
||||
---
|
||||
|
||||
# Critères d'évaluation
|
||||
|
||||
L'évaluation portera sur les points suivants :
|
||||
|
||||
| Critère | Points d'attention |
|
||||
| ------------------- | ---------------------------------------------------------------------- |
|
||||
| Base SQLite | Création correcte et accès fonctionnel |
|
||||
| API REST | Routes opérationnelles et réponses JSON conformes |
|
||||
| CRUD | Ajout, consultation, modification et suppression fonctionnels |
|
||||
| Interface Jinja2 | Utilisation correcte des variables, boucles et conditions |
|
||||
| Formulaire HTML | Enregistrement correct des données dans SQLite |
|
||||
| Client `requests` | Appels HTTP correctement réalisés |
|
||||
| Gestion des erreurs | Timeout, vérification des réponses et traitement propre des exceptions |
|
||||
| Qualité du code | Organisation, lisibilité et séparation des responsabilités |
|
||||
@@ -0,0 +1,80 @@
|
||||
# Exo 3.4 "Consommer l'API" - BOICHÉ Gauvain
|
||||
|
||||
## Usage de l'IA
|
||||
|
||||
### Raison 1
|
||||
|
||||
Pour convertir la structure de SQLAlchemy en SQLite. Parce que, comme un imbécile, j'ai tout fait en SQLAlchemy, sans percuter.
|
||||
|
||||
#### Ce que j'en ai retenu
|
||||
|
||||
La somme de la conversion était colossale.
|
||||
|
||||
Déjà, de nombreuses méthodes (pas au sens Python du terme) m'étant inconnues ont été intégrées. J'ai lu religieusement pour comprendre, et ça va, le sens ne m'échappe pas.
|
||||
|
||||
Celle qui m'a fait le plus tiquer c'est `flash()`. Les strings disent `Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call get_flashed_messages.` C'est spécial. Je me rends compte aussi que mes propres fonctions n'ont pas de """docstring""", mais pour autant, je pense ne pas en avoir besoin.
|
||||
|
||||
Un de mes mentors m'avait dit : "un bon code se passe de commentaire". Et ma convention de nommage semble assez exemplaire pour s'en passer.
|
||||
|
||||
### Raison 2
|
||||
|
||||
Pour générer les fichiers .env et .gitignore, parce que ça me saoule aussi de fouiller dans mes dossiers jusqu'à satsifaction. Et faire une méthode pour ne rien lancer sans ces valeurs.
|
||||
|
||||
#### Ce que j'en ai retenu
|
||||
|
||||
Y a rien à retenir de particulier. Sauf peut-être un genre de dossier général à la racine de mes projets avec des patrons tout fait.
|
||||
|
||||
Quand à la fonction `_required_env`, à la base j'avais fait un simple `try: except:` mais ça semble mieux comme ça.
|
||||
|
||||
Et par ricochet, j'ai vu qu'on pouvait définir des variables dans l'application "Flask" avec `.config`.
|
||||
|
||||
### Raison 3
|
||||
|
||||
Générer du code CSS.
|
||||
|
||||
#### Ce que j'en ai retenu
|
||||
|
||||
La raison pour laquelle j'ai lâché mes cours CodeCademy sur le Frontend. Ca me gonfle.
|
||||
|
||||
### Raison 4
|
||||
|
||||
Choisir de faire ou non une réinitialisation de la DB en relançant app.py
|
||||
|
||||
#### Ce que j'en ai retenu
|
||||
|
||||
J'ai plusieurs fois voulu faire un choix multiples en Python sans jamais me renseigner dessus, et j'étais trop avancé pour refactoriser ça moi-même. Visiblement ça n'existe pas trop dans l'état, ça marche avec un "O/N" classique. Peut-être existe-t-il une solution (choisir avec les flèches du clavier) mais ça demande certainement un genre de moteur visuel. Je ne sais pas du moins. Je n'ai pas insisté, mais de fait je me trouve stupide de demander à l'IA ce que je savais déjà faire. Après, j'ignorais que je connaissais déjà la seule solution, donc au moins je retiens que Python ne gère pas forcément tout.
|
||||
|
||||
### Raison 5
|
||||
|
||||
Ecrire les commandes de test, notamment sur la robustesse.
|
||||
|
||||
#### Ce que j'en ai retenu
|
||||
|
||||
La sortie JSON formatée par python m'intéresse. Je ne pensais pas que Python pouvait servir aussi à ça. Dans le quotidien ça peut être utile à savoir.
|
||||
|
||||
Et surtout, je voulais juste une liste de commandes curl, mais l'IA m'a proposé directement après, sans lui demander, un script .sh (joint). Pourquoi m'embêter avec un .md et des copier/coller ? Je dois y repenser à chaque fois. Faire des sorties automatiques. Je le fais au travail mais jamais en exercice.
|
||||
|
||||
Partisan du moindre effort ? Oui, et ça fait de moi un bon informaticien.
|
||||
|
||||
### Raison 6
|
||||
|
||||
Téléversement de mon projet finalisé (le .md de l'exercice, les .py et les .html ainsi qu'une capture d'écran de l'arborescence) sur trois IA spécialisées différentes (QwenCoder 3.7 Plus, MiniMax M3, Kimi K2.6 [on le sent que je contrebalance la domination américaine par de la domination chinoise, ou pas du tout ?]) avec une demande de revue.
|
||||
|
||||
#### Les résultats
|
||||
|
||||
Les trois IA fournissent des rapports cohérents (ce qui est rassurant) même si chacune courbe son jugement par son paradigme interne :
|
||||
|
||||
- Qwen se concentre sur la base
|
||||
- Kimi relève les soucis d'imports circulaires et les potentiels points de blocage concrets actuels
|
||||
- MiniMax a relevé les potentiels problèmes futurs en anticipant une application qui prend de l'ampleur
|
||||
|
||||
Les bogues relevés :
|
||||
|
||||
- Sérialisation JSON menant à une potentielle `TypeError` (que je n'ai pas constatée)
|
||||
- Fonctions critiques potentiellement bloquantes
|
||||
- Fonctions critiques dans des boucles non sécurisées
|
||||
- Coquilles et oublis de lignes à rajouter/retirer
|
||||
|
||||
#### Ce que j'ai modifié en conséquence
|
||||
|
||||
Corrections des bogues critiques, re-test et validation.
|
||||
@@ -0,0 +1,234 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, jsonify, request, render_template, redirect, url_for, flash
|
||||
|
||||
from classes import Equipment, Interface
|
||||
from database import (
|
||||
init_database,
|
||||
reset_database,
|
||||
prompt_reset,
|
||||
fetch_all_equipments,
|
||||
fetch_equipment_by_id,
|
||||
insert_equipment,
|
||||
update_equipment,
|
||||
delete_equipment,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def _required_env(key: str) -> str:
|
||||
value = os.environ.get(key)
|
||||
if not value:
|
||||
raise RuntimeError(
|
||||
f"Variable d'environnement {key!r} manquante. "
|
||||
f"Vérifiez votre fichier .env (voir .env.example)."
|
||||
)
|
||||
return value
|
||||
|
||||
app.config["SECRET_KEY"] = _required_env("FLASK_SECRET_KEY")
|
||||
app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "False").lower() in {"1", "true", "yes"}
|
||||
app.config["DATABASE_PATH"] = os.environ.get("DATABASE_PATH", "databases/inventory.db")
|
||||
|
||||
ALLOWED_TYPES = {"Server", "Instance", "Switch", "Gateway", "Router", "WAF", "Database"}
|
||||
|
||||
def _parse_form_equipment(form) -> tuple[Equipment | None, str | None]:
|
||||
hostname = (form.get("hostname") or "").strip()
|
||||
if not hostname:
|
||||
return None, "Hostname requis."
|
||||
|
||||
eq_type = form.get("type", "Server")
|
||||
if eq_type not in ALLOWED_TYPES:
|
||||
return None, f"Type invalide : {eq_type}."
|
||||
|
||||
ip = (form.get("ip_address") or "").strip() or "255.255.255.0"
|
||||
is_active = form.get("is_active") == "on"
|
||||
|
||||
latency_raw = (form.get("latency") or "").strip()
|
||||
latency = None
|
||||
if latency_raw:
|
||||
try:
|
||||
latency = float(latency_raw)
|
||||
if latency < 0:
|
||||
return None, "Latence doit être positive."
|
||||
except ValueError:
|
||||
return None, "Latence invalide."
|
||||
|
||||
ports = form.getlist("ports[]")
|
||||
services = form.getlist("services[]")
|
||||
interfaces: list[Interface] = []
|
||||
for port_raw, service in zip(ports, services):
|
||||
if not port_raw:
|
||||
continue
|
||||
try:
|
||||
port = int(port_raw)
|
||||
if not (0 <= port <= 65535):
|
||||
return None, f"Port invalide : {port_raw}."
|
||||
except ValueError:
|
||||
return None, f"Port non numérique : {port_raw}."
|
||||
if not service.strip():
|
||||
return None, "Service requis pour chaque interface."
|
||||
interfaces.append(Interface(port=port, service=service.strip()))
|
||||
|
||||
return Equipment(
|
||||
hostname=hostname,
|
||||
type=eq_type,
|
||||
ip_address=ip,
|
||||
is_active=is_active,
|
||||
latency=latency,
|
||||
interfaces=interfaces,
|
||||
), None
|
||||
|
||||
def _equipment_to_dict(eq: Equipment) -> dict:
|
||||
return {
|
||||
"id": eq.id,
|
||||
"hostname": eq.hostname,
|
||||
"type": eq.type,
|
||||
"ip_address": eq.ip_address,
|
||||
"is_active": eq.is_active,
|
||||
"latency": eq.latency,
|
||||
"interfaces": [
|
||||
{"id": i.id, "port": i.port, "service": i.service}
|
||||
for i in eq.interfaces
|
||||
],
|
||||
}
|
||||
|
||||
# Partie FRONT
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/equipments', methods=['GET'])
|
||||
def equipments_page():
|
||||
equipments = fetch_all_equipments()
|
||||
return render_template('equipments.html', equipments=equipments, types=sorted(ALLOWED_TYPES))
|
||||
|
||||
@app.route('/equipments', methods=['POST'])
|
||||
def create_equipment_form():
|
||||
eq, err = _parse_form_equipment(request.form)
|
||||
if err:
|
||||
flash(err, "error")
|
||||
return redirect(url_for('equipments_page'))
|
||||
insert_equipment(eq)
|
||||
flash(f"Équipement « {eq.hostname} » créé.", "success")
|
||||
return redirect(url_for('equipments_page'))
|
||||
|
||||
@app.route('/equipments/<int:equipment_id>/toggle', methods=['POST'])
|
||||
def toggle_equipment(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
flash(f"Équipement {equipment_id} introuvable.", "error")
|
||||
return redirect(url_for('equipments_page'))
|
||||
eq.is_active = not eq.is_active
|
||||
update_equipment(eq)
|
||||
flash(f"État de « {eq.hostname} » mis à jour.", "success")
|
||||
return redirect(url_for('equipments_page'))
|
||||
|
||||
@app.route('/equipments/<int:equipment_id>/delete', methods=['POST'])
|
||||
def delete_equipment_form(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
flash(f"Équipement {equipment_id} introuvable.", "error")
|
||||
return redirect(url_for('equipments_page'))
|
||||
delete_equipment(equipment_id)
|
||||
flash(f"Équipement « {eq.hostname} » supprimé.", "success")
|
||||
return redirect(url_for('equipments_page'))
|
||||
|
||||
# Partie API
|
||||
|
||||
@app.get("/api/equipments")
|
||||
def list_equipments_api():
|
||||
return jsonify([_equipment_to_dict(eq) for eq in fetch_all_equipments()])
|
||||
|
||||
@app.get("/api/equipments/<int:equipment_id>")
|
||||
def get_equipment_api(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
return jsonify({"error": "Équipement introuvable."}), 404
|
||||
return jsonify(_equipment_to_dict(eq))
|
||||
|
||||
@app.post("/api/equipments")
|
||||
def create_equipment_api():
|
||||
data = request.get_json(silent=True) or {}
|
||||
hostname = (data.get("hostname") or "").strip()
|
||||
if not hostname:
|
||||
return jsonify({"error": "Hostname requis."}), 400
|
||||
eq_type = data.get("type", "Server")
|
||||
if eq_type not in ALLOWED_TYPES:
|
||||
return jsonify({"error": f"Type invalide : {eq_type}."}), 400
|
||||
eq = Equipment(
|
||||
hostname=hostname,
|
||||
type=eq_type,
|
||||
ip_address=(data.get("ip_address") or "255.255.255.0").strip(),
|
||||
is_active=bool(data.get("is_active", True)),
|
||||
latency=data.get("latency"),
|
||||
interfaces=[],
|
||||
)
|
||||
new_id = insert_equipment(eq)
|
||||
eq.id = new_id
|
||||
return jsonify(_equipment_to_dict(eq)), 201
|
||||
|
||||
@app.put("/api/equipments/<int:equipment_id>")
|
||||
def update_equipment_api(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
hostname = (data.get("hostname") or "").strip()
|
||||
if not hostname:
|
||||
return jsonify({"error": "Hostname requis."}), 400
|
||||
|
||||
eq_type = data.get("type", "Server")
|
||||
if eq_type not in ALLOWED_TYPES:
|
||||
return jsonify({"error": f"Type invalide : {eq_type}."}), 400
|
||||
|
||||
latency = data.get("latency")
|
||||
if latency is not None:
|
||||
try:
|
||||
latency = float(latency)
|
||||
if latency < 0:
|
||||
return jsonify({"error": "Latence doit être positive."}), 400
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Latence invalide."}), 400
|
||||
|
||||
eq.hostname = hostname
|
||||
eq.type = eq_type
|
||||
eq.ip_address = (data.get("ip_address") or "255.255.255.0").strip()
|
||||
eq.is_active = bool(data.get("is_active", True))
|
||||
eq.latency = latency
|
||||
|
||||
update_equipment(eq)
|
||||
return jsonify(_equipment_to_dict(eq)), 200
|
||||
|
||||
@app.patch("/api/equipments/<int:equipment_id>/toggle")
|
||||
def toggle_equipment_api(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404
|
||||
eq.is_active = not eq.is_active
|
||||
update_equipment(eq)
|
||||
return jsonify(_equipment_to_dict(eq)), 200
|
||||
|
||||
@app.delete("/api/equipments/<int:equipment_id>")
|
||||
def delete_equipment_api(equipment_id: int):
|
||||
eq = fetch_equipment_by_id(equipment_id)
|
||||
if eq is None:
|
||||
return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404
|
||||
delete_equipment(equipment_id)
|
||||
return jsonify({"message": f"Équipement « {eq.hostname} » supprimé."}), 200
|
||||
|
||||
if __name__ == '__main__':
|
||||
host = os.environ.get("FLASK_HOST", "127.0.0.1")
|
||||
port = int(os.environ.get("FLASK_PORT", "5000"))
|
||||
|
||||
if prompt_reset():
|
||||
reset_database()
|
||||
print("Base réinitialisée.")
|
||||
else:
|
||||
init_database()
|
||||
|
||||
app.run(host=host, port=port, debug=app.config["DEBUG"])
|
||||
@@ -0,0 +1,19 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
@dataclass
|
||||
class Interface:
|
||||
id: Optional[int] = None
|
||||
port: int = 0
|
||||
service: str = "[SERVICE]"
|
||||
equipment_id: Optional[int] = None
|
||||
|
||||
@dataclass
|
||||
class Equipment:
|
||||
id: Optional[int] = None
|
||||
hostname: str = "[HOSTNAME]"
|
||||
type: str = "Server"
|
||||
ip_address: str = "255.255.255.0"
|
||||
is_active: bool = True
|
||||
latency: Optional[float] = None
|
||||
interfaces: List[Interface] = field(default_factory=list)
|
||||
@@ -0,0 +1,92 @@
|
||||
import sys
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://127.0.0.1:5000"
|
||||
TIMEOUT = 5
|
||||
|
||||
def list_equipments() -> list[dict] | None:
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/api/equipments", timeout=TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"[ERREUR] Délai d'attente dépassé ({TIMEOUT}s) lors de GET /api/equipments.")
|
||||
return None
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"[ERREUR] Impossible de joindre le serveur {BASE_URL}. Est-il démarré ?")
|
||||
return None
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"[ERREUR] Réponse HTTP invalide : {e}")
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"[ERREUR] Échec de la requête : {e}")
|
||||
return None
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
print("[ERREUR] Réponse non-JSON reçue du serveur.")
|
||||
return None
|
||||
|
||||
if not isinstance(data, list):
|
||||
print("[ERREUR] Format de réponse inattendu (liste attendue).")
|
||||
return None
|
||||
return data
|
||||
|
||||
def add_equipment(hostname: str, type_: str, ip_address: str) -> dict | None:
|
||||
payload = {
|
||||
"hostname": hostname,
|
||||
"type": type_,
|
||||
"ip_address": ip_address,
|
||||
"is_active": True,
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/equipments",
|
||||
json=payload,
|
||||
timeout=TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"[ERREUR] Délai d'attente dépassé ({TIMEOUT}s) lors de POST /api/equipments.")
|
||||
return None
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"[ERREUR] Impossible de joindre le serveur {BASE_URL}.")
|
||||
return None
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(f"[ERREUR] Réponse HTTP invalide : {e}")
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"[ERREUR] Échec de la requête : {e}")
|
||||
return None
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
print("[ERREUR] Réponse non-JSON reçue après création.")
|
||||
return None
|
||||
|
||||
def main() -> int:
|
||||
print("=== Liste initiale ===")
|
||||
initial = list_equipments()
|
||||
if initial is None:
|
||||
return 1
|
||||
for eq in initial:
|
||||
print(f" #{eq['id']} {eq['hostname']} ({eq['type']}) - {'actif' if eq['is_active'] else 'inactif'}")
|
||||
|
||||
print("\n=== Ajout d'un équipement ===")
|
||||
new_eq = add_equipment("client-test-01", "Router", "10.0.99.1")
|
||||
if new_eq is None:
|
||||
return 1
|
||||
print(f" Créé : #{new_eq['id']} {new_eq['hostname']}")
|
||||
|
||||
print("\n=== Liste mise à jour ===")
|
||||
updated = list_equipments()
|
||||
if updated is None:
|
||||
return 1
|
||||
for eq in updated:
|
||||
print(f" #{eq['id']} {eq['hostname']} ({eq['type']}) - {'actif' if eq['is_active'] else 'inactif'}")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,157 @@
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from classes import Equipment, Interface
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
DB_DIR = BASE_DIR / "databases"
|
||||
DB_PATH = DB_DIR / "inventory.db"
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS equipments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hostname TEXT NOT NULL DEFAULT '[HOSTNAME]',
|
||||
type TEXT NOT NULL DEFAULT 'Server',
|
||||
ip_address TEXT NOT NULL DEFAULT '255.255.255.0',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
latency REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS interfaces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
port INTEGER NOT NULL,
|
||||
service TEXT NOT NULL DEFAULT '[SERVICE]',
|
||||
equipment_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (equipment_id) REFERENCES equipments(id) ON DELETE CASCADE
|
||||
);
|
||||
"""
|
||||
def _ensure_db_dir() -> None:
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@contextmanager
|
||||
def get_connection():
|
||||
_ensure_db_dir()
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def init_database() -> None:
|
||||
with get_connection() as conn:
|
||||
conn.executescript(SCHEMA)
|
||||
conn.commit()
|
||||
|
||||
def execute_query(query: str, params: Iterable[Any] = ()) -> list[sqlite3.Row]:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(query, tuple(params))
|
||||
return cursor.fetchall()
|
||||
|
||||
def execute_modification(query: str, params: Iterable[Any] = ()) -> int:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(query, tuple(params))
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
|
||||
def _row_to_equipment(row: sqlite3.Row) -> Equipment:
|
||||
return Equipment(
|
||||
id=row["id"],
|
||||
hostname=row["hostname"],
|
||||
type=row["type"],
|
||||
ip_address=row["ip_address"],
|
||||
is_active=bool(row["is_active"]),
|
||||
latency=row["latency"],
|
||||
)
|
||||
|
||||
def _row_to_interface(row: sqlite3.Row) -> Interface:
|
||||
return Interface(
|
||||
id=row["id"],
|
||||
port=row["port"],
|
||||
service=row["service"],
|
||||
equipment_id=row["equipment_id"],
|
||||
)
|
||||
|
||||
def fetch_all_equipments() -> list[Equipment]:
|
||||
rows = execute_query(
|
||||
"SELECT id, hostname, type, ip_address, is_active, latency "
|
||||
"FROM equipments ORDER BY id"
|
||||
)
|
||||
equipments = [_row_to_equipment(r) for r in rows]
|
||||
for eq in equipments:
|
||||
eq.interfaces = fetch_interfaces_for_equipment(eq.id) #type: ignore
|
||||
return equipments
|
||||
|
||||
def fetch_equipment_by_id(equipment_id: int) -> Optional[Equipment]:
|
||||
rows = execute_query(
|
||||
"SELECT id, hostname, type, ip_address, is_active, latency "
|
||||
"FROM equipments WHERE id = ?",
|
||||
(equipment_id,),
|
||||
)
|
||||
if not rows:
|
||||
return None
|
||||
eq = _row_to_equipment(rows[0])
|
||||
eq.interfaces = fetch_interfaces_for_equipment(eq.id) #type: ignore
|
||||
return eq
|
||||
|
||||
def fetch_interfaces_for_equipment(equipment_id: int) -> list[Interface]:
|
||||
rows = execute_query(
|
||||
"SELECT id, port, service, equipment_id FROM interfaces "
|
||||
"WHERE equipment_id = ? ORDER BY id",
|
||||
(equipment_id,),
|
||||
)
|
||||
return [_row_to_interface(r) for r in rows]
|
||||
|
||||
def insert_equipment(eq: Equipment) -> int:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
"INSERT INTO equipments (hostname, type, ip_address, is_active, latency) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(eq.hostname, eq.type, eq.ip_address, int(eq.is_active), eq.latency),
|
||||
)
|
||||
eq_id = cursor.lastrowid
|
||||
eq.id = eq_id # <-- ajout
|
||||
for iface in eq.interfaces:
|
||||
conn.execute(
|
||||
"INSERT INTO interfaces (port, service, equipment_id) "
|
||||
"VALUES (?, ?, ?)",
|
||||
(iface.port, iface.service, eq_id),
|
||||
)
|
||||
conn.commit()
|
||||
return eq_id #type:ignore
|
||||
|
||||
def update_equipment(eq: Equipment) -> bool:
|
||||
return execute_modification(
|
||||
"UPDATE equipments SET hostname=?, type=?, ip_address=?, "
|
||||
"is_active=?, latency=? WHERE id=?",
|
||||
(eq.hostname, eq.type, eq.ip_address, int(eq.is_active),
|
||||
eq.latency, eq.id),
|
||||
) > 0
|
||||
|
||||
def delete_equipment(equipment_id: int) -> bool:
|
||||
return execute_modification(
|
||||
"DELETE FROM equipments WHERE id = ?", (equipment_id,)
|
||||
) > 0
|
||||
|
||||
def reset_database() -> None:
|
||||
from init_db import main as seed
|
||||
|
||||
if DB_PATH.exists():
|
||||
DB_PATH.unlink()
|
||||
init_database()
|
||||
seed()
|
||||
|
||||
def prompt_reset() -> bool:
|
||||
while True:
|
||||
choice = input("Réinitialiser la base de données ? [o/N] : ").strip().lower()
|
||||
if choice in {"o", "oui", "y", "yes"}:
|
||||
return True
|
||||
if choice in {"", "n", "non", "no"}:
|
||||
return False
|
||||
print("Réponse invalide. Saisissez 'o' (oui) ou 'n' (non).")
|
||||
|
||||
# Gestion API
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from random import choice, randint
|
||||
from database import init_database, insert_equipment, fetch_all_equipments
|
||||
from classes import Equipment, Interface
|
||||
|
||||
NUMBER_OF_EQUIPMENTS_TO_POPULATE = 15
|
||||
|
||||
COMMON_SERVICES: dict[str, int] = {
|
||||
"FTP": 21,
|
||||
"SSH": 22,
|
||||
"Telnet": 23,
|
||||
"SMTP": 25,
|
||||
"DNS": 53,
|
||||
"HTTP": 80,
|
||||
"POP3": 110,
|
||||
"NetBIOS": 139,
|
||||
"IMAP": 143,
|
||||
"HTTPS": 443,
|
||||
"LDAPS": 636,
|
||||
"RDP": 3389,
|
||||
}
|
||||
|
||||
TYPES = [
|
||||
"Server",
|
||||
"Instance",
|
||||
"Switch",
|
||||
"Gateway",
|
||||
"Router",
|
||||
"WAF",
|
||||
"Database",
|
||||
]
|
||||
|
||||
def random_ipv4() -> str:
|
||||
return ".".join(str(randint(0, 255)) for _ in range(4))
|
||||
|
||||
def load_words(path: str = "wordlist.txt") -> list[str]:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return [line.strip() for line in f if line.strip()]
|
||||
|
||||
def random_hostname(words: list[str]) -> str:
|
||||
triplet = "-".join(choice(words) for _ in range(3))
|
||||
return f"{triplet}-{randint(1, 999):03d}"
|
||||
|
||||
def random_interfaces() -> list[Interface]:
|
||||
count = randint(2, 5)
|
||||
services = list(COMMON_SERVICES.items())
|
||||
return [Interface(port=port, service=svc) for svc, port in
|
||||
(choice(services) for _ in range(count))]
|
||||
|
||||
def main(n: int = NUMBER_OF_EQUIPMENTS_TO_POPULATE) -> None:
|
||||
init_database()
|
||||
if fetch_all_equipments():
|
||||
print("Base déjà peuplée, peuplement ignoré.")
|
||||
return
|
||||
words = load_words()
|
||||
for _ in range(n):
|
||||
insert_equipment(Equipment(
|
||||
hostname=random_hostname(words),
|
||||
type=choice(TYPES),
|
||||
ip_address=random_ipv4(),
|
||||
is_active=True,
|
||||
latency=round(randint(1, 500) / 10, 1),
|
||||
interfaces=random_interfaces(),
|
||||
))
|
||||
print(f"{n} équipements insérés.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "3_4_API_REST"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "USAGE-IA.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"dotenv>=0.9.9",
|
||||
"flask>=3.1.3",
|
||||
"requests>=2.34.2",
|
||||
"sqlalchemy>=2.0.51",
|
||||
]
|
||||
@@ -0,0 +1,306 @@
|
||||
# Exo 3.4 "Consommer l'API" - BOICHÉ Gauvain
|
||||
|
||||
## Requêtes type
|
||||
|
||||
### Lister
|
||||
|
||||
```
|
||||
# Liste complète
|
||||
curl -i http://127.0.0.1:5000/api/equipments
|
||||
|
||||
# Equipement précis
|
||||
curl -i http://127.0.0.1:5000/api/equipments/1
|
||||
|
||||
# Equipement inexistant
|
||||
curl -i http://127.0.0.1:5000/api/equipments/9999
|
||||
|
||||
# Sortie formatée
|
||||
curl -s http://127.0.0.1:5000/api/equipments | python -m json.tool
|
||||
|
||||
# Compter les équipements
|
||||
curl -s http://127.0.0.1:5000/api/equipments | python -c "import json,sys; print(len(json.load(sys.stdin)))"
|
||||
|
||||
# Filtrage par champ
|
||||
curl -s http://127.0.0.1:5000/api/equipments | jq '.[] | select(.is_active==true) | {id, hostname}'
|
||||
```
|
||||
|
||||
### Créer
|
||||
|
||||
```
|
||||
# Normal
|
||||
curl -i -X POST http://127.0.0.1:5000/api/equipments \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"curl-test-01","type":"router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}'
|
||||
|
||||
# Mal formaté
|
||||
curl -i -X POST http://127.0.0.1:5000/api/equipments \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{hostname: "sans-guillemets"}'
|
||||
|
||||
# Avec mentions obligatoires manquantes
|
||||
curl -i -X POST http://127.0.0.1:5000/api/equipments \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"router"}'
|
||||
```
|
||||
|
||||
### Patch partiel (actif <-> inactif)
|
||||
|
||||
```
|
||||
curl -i -X PATCH http://127.0.0.1:5000/api/equipments/1/toggle
|
||||
```
|
||||
|
||||
### Mise à jour
|
||||
|
||||
```
|
||||
curl -i -X PUT http://127.0.0.1:5000/api/equipments/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"serveur-prod-01","type":"server","ip_address":"10.0.10.1","is_active":true,"latency":8.3}'
|
||||
```
|
||||
|
||||
### Supprimer
|
||||
|
||||
```
|
||||
# Autorisé
|
||||
curl -i -X DELETE http://127.0.0.1:5000/api/equipments/1
|
||||
|
||||
# Interdit
|
||||
curl -i -X DELETE http://127.0.0.1:5000/api/equipments
|
||||
```
|
||||
|
||||
## Réponses
|
||||
|
||||
### Lister
|
||||
|
||||
```
|
||||
> 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: Wed, 08 Jul 2026 12:58:59 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 6850
|
||||
Connection: close
|
||||
|
||||
[
|
||||
{
|
||||
"hostname": "bird-dawn-peas-031",
|
||||
"id": 1,
|
||||
"interfaces": [
|
||||
{
|
||||
"id": 1,
|
||||
"port": 25,
|
||||
"service": "SMTP"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"port": 21,
|
||||
"service": "FTP"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"port": 80,
|
||||
"service": "HTTP"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"port": 21,
|
||||
"service": "FTP"
|
||||
}
|
||||
],
|
||||
"ip_address": "242.48.158.201",
|
||||
"is_active": true,
|
||||
"latency": 33.6,
|
||||
"type": "Router"
|
||||
}, [...]
|
||||
|
||||
> curl -i http://127.0.0.1:5000/api/equipments/12
|
||||
HTTP/1.1 200 OK
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:21:47 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 314
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"hostname": "barn-lamps-bottle-151",
|
||||
"id": 12,
|
||||
"interfaces": [
|
||||
{
|
||||
"id": 42,
|
||||
"port": 53,
|
||||
"service": "DNS"
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"port": 139,
|
||||
"service": "NetBIOS"
|
||||
}
|
||||
],
|
||||
"ip_address": "76.143.32.122",
|
||||
"is_active": true,
|
||||
"latency": 42.1,
|
||||
"type": "Server"
|
||||
}
|
||||
|
||||
> curl -i http://127.0.0.1:5000/api/equipments/1267
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:22:01 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 46
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"error": "\u00c9quipement introuvable."
|
||||
}
|
||||
|
||||
> curl -s http://127.0.0.1:5000/api/equipments | python -c "import json,sys; print(len(json.load(sys.stdin)))"
|
||||
15
|
||||
```
|
||||
|
||||
### Créer
|
||||
|
||||
```
|
||||
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname":"curl-test-01","type":"Router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}'
|
||||
HTTP/1.1 201 CREATED
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:23:15 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 154
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"hostname": "curl-test-01",
|
||||
"id": 16,
|
||||
"interfaces": [],
|
||||
"ip_address": "10.0.50.1",
|
||||
"is_active": true,
|
||||
"latency": 12.5,
|
||||
"type": "Router"
|
||||
}
|
||||
|
||||
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname":"curl-test-01","type":"router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}'
|
||||
HTTP/1.1 400 BAD REQUEST
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:23:04 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 41
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"error": "Type invalide : router."
|
||||
}
|
||||
|
||||
> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"type":"Router"}'
|
||||
HTTP/1.1 400 BAD REQUEST
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:23:58 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 34
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"error": "Hostname requis."
|
||||
}
|
||||
```
|
||||
|
||||
### Patch partiel (actif <-> inactif)
|
||||
|
||||
```
|
||||
> curl -i -X PATCH http://127.0.0.1:5000/api/equipments/2/toggle
|
||||
HTTP/1.1 200 OK
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:46:11 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 387
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"hostname": "gulf-manage-order-022",
|
||||
"id": 2,
|
||||
"interfaces": [
|
||||
{
|
||||
"id": 3,
|
||||
"port": 21,
|
||||
"service": "FTP"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"port": 139,
|
||||
"service": "NetBIOS"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"port": 143,
|
||||
"service": "IMAP"
|
||||
}
|
||||
],
|
||||
"ip_address": "183.139.112.112",
|
||||
"is_active": false,
|
||||
"latency": 28.1,
|
||||
"type": "Database"
|
||||
}
|
||||
```
|
||||
|
||||
### Mise à jour
|
||||
|
||||
```
|
||||
> curl -i -X PUT http://127.0.0.1:5000/api/equipments/1 -H "Content-Type: application/json" -d '{"hostname":"serveur-prod-01","type":"Server","ip_address":"10.0.10.1","is_active":true,"latency":8.3}'
|
||||
HTTP/1.1 200 OK
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:49:06 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 297
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"hostname": "serveur-prod-01",
|
||||
"id": 1,
|
||||
"interfaces": [
|
||||
{
|
||||
"id": 1,
|
||||
"port": 53,
|
||||
"service": "DNS"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"port": 3389,
|
||||
"service": "RDP"
|
||||
}
|
||||
],
|
||||
"ip_address": "10.0.10.1",
|
||||
"is_active": true,
|
||||
"latency": 8.3,
|
||||
"type": "Server"
|
||||
}
|
||||
```
|
||||
|
||||
### Supprimer
|
||||
|
||||
```
|
||||
> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/1
|
||||
HTTP/1.1 200 OK
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:49:49 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 80
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"message": "\u00c9quipement \u00ab serveur-prod-01 \u00bb supprim\u00e9."
|
||||
}
|
||||
|
||||
> curl -i -X DELETE http://127.0.0.1:5000/api/equipments
|
||||
HTTP/1.1 405 METHOD NOT ALLOWED
|
||||
Server: Werkzeug/3.1.8 Python/3.14.6
|
||||
Date: Wed, 08 Jul 2026 13:50:04 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Allow: OPTIONS, POST, HEAD, GET
|
||||
Content-Length: 153
|
||||
Connection: close
|
||||
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>405 Method Not Allowed</title>
|
||||
<h1>Method Not Allowed</h1>
|
||||
<p>The method is not allowed for the requested URL.</p>
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
:root {
|
||||
--bg: #f5f5f7;
|
||||
--card: #ffffff;
|
||||
--border: #d2d2d7;
|
||||
--accent: #0071e3;
|
||||
--danger: #ff3b30;
|
||||
--success: #34c759;
|
||||
}
|
||||
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); margin: 2rem; }
|
||||
nav { margin-bottom: 1.5rem; }
|
||||
nav a { color: var(--accent); text-decoration: none; margin-right: 1rem; }
|
||||
|
||||
.flash { padding: .75rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
|
||||
.flash-success { background: #d1f5d3; color: #0a6b1f; }
|
||||
.flash-error { background: #ffd5d2; color: #a0190d; }
|
||||
|
||||
form { background: var(--card); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border); }
|
||||
form label { display: block; margin-bottom: .75rem; }
|
||||
form input, form select { padding: .5rem; border: 1px solid var(--border); border-radius: 4px; }
|
||||
|
||||
fieldset { margin-top: 1rem; border: 1px solid var(--border); padding: 1rem; border-radius: 6px; }
|
||||
.interface-row { display: flex; gap: .5rem; margin-bottom: .5rem; }
|
||||
.interface-row input { flex: 1; }
|
||||
|
||||
.equipment-grid { list-style: none; padding: 0; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
||||
.equipment { background: var(--card); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); }
|
||||
.equipment.inactive { opacity: .6; }
|
||||
.equipment header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.badge { background: var(--accent); color: white; padding: .2rem .6rem; border-radius: 12px; font-size: .8rem; }
|
||||
.equipment footer { display: flex; gap: .5rem; margin-top: 1rem; }
|
||||
.equipment footer form { flex: 1; padding: 0; border: none; background: none; }
|
||||
.equipment button { width: 100%; padding: .5rem; border: none; border-radius: 4px; cursor: pointer; color: white; }
|
||||
.btn-toggle { background: var(--accent); }
|
||||
.btn-delete { background: var(--danger); }
|
||||
.empty { color: #6e6e73; font-style: italic; }
|
||||
|
||||
.equipments-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.equipments-table th,
|
||||
.equipments-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
.equipments-table thead {
|
||||
background: #f4f4f4;
|
||||
}
|
||||
.equipments-table .row-inactive {
|
||||
opacity: 0.6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.status {
|
||||
font-weight: 600;
|
||||
}
|
||||
.status-active { color: #1a7f37; }
|
||||
.status-inactive { color: #b42318; }
|
||||
.actions form { display: inline-block; margin-right: 0.25rem; }
|
||||
.interfaces-list { margin: 0; padding-left: 1rem; }
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Gestion du parc{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/equipments.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav><a href="{{ url_for('index') }}">Accueil</a> | <a href="{{ url_for('equipments_page') }}">Équipements</a></nav>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{ category }}" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Équipements{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Inventaire des équipements</h1>
|
||||
|
||||
<section>
|
||||
<h2>Ajouter un équipement</h2>
|
||||
<form method="post" action="{{ url_for('create_equipment_form') }}">
|
||||
<label>Hostname <input type="text" name="hostname" required></label>
|
||||
<label>Type
|
||||
<select name="type" required>
|
||||
{% for t in types %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Adresse IP <input type="text" name="ip_address" required></label>
|
||||
<label>Latence (ms) <input type="number" name="latency" step="0.1" min="0"></label>
|
||||
<label><input type="checkbox" name="is_active" checked> Actif</label>
|
||||
|
||||
<fieldset>
|
||||
<legend>Interfaces</legend>
|
||||
<div class="interface-row">
|
||||
<input type="number" name="ports[]" placeholder="Port" min="0" max="65535">
|
||||
<input type="text" name="services[]" placeholder="Service">
|
||||
</div>
|
||||
<div class="interface-row">
|
||||
<input type="number" name="ports[]" placeholder="Port" min="0" max="65535">
|
||||
<input type="text" name="services[]" placeholder="Service">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Créer</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Équipements enregistrés ({{ equipments|length }})</h2>
|
||||
{% if equipments %}
|
||||
<table class="equipments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Hostname</th>
|
||||
<th>Type</th>
|
||||
<th>Adresse IP</th>
|
||||
<th>Latence</th>
|
||||
<th>État</th>
|
||||
<th>Interfaces</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for eq in equipments %}
|
||||
<tr class="{% if not eq.is_active %}row-inactive{% else %}row-active{% endif %}">
|
||||
<td>{{ eq.id }}</td>
|
||||
<td>{{ eq.hostname }}</td>
|
||||
<td><span class="badge">{{ eq.type }}</span></td>
|
||||
<td>{{ eq.ip_address }}</td>
|
||||
<td>{{ eq.latency if eq.latency is not none else '-' }} ms</td>
|
||||
<td>
|
||||
{% if eq.is_active %}
|
||||
<span class="status status-active">Actif</span>
|
||||
{% else %}
|
||||
<span class="status status-inactive">Inactif</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if eq.interfaces %}
|
||||
<ul class="interfaces-list">
|
||||
{% for i in eq.interfaces %}
|
||||
<li>{{ i.service }} : {{ i.port }}/tcp</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
<form method="post" action="{{ url_for('toggle_equipment', equipment_id=eq.id) }}">
|
||||
<button type="submit" class="btn-toggle">
|
||||
{% if eq.is_active %}Désactiver{% else %}Activer{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('delete_equipment_form', equipment_id=eq.id) }}"
|
||||
onsubmit="return confirm('Confirmer la suppression de {{ eq.hostname }} ?');">
|
||||
<button type="submit" class="btn-delete">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">Aucun équipement enregistré.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Accueil{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Bienvenue</h1>
|
||||
<p><a href="{{ url_for('equipments_page') }}">Accéder à l'inventaire</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
BASE="http://127.0.0.1:5000/api/equipments"
|
||||
|
||||
echo "=== GET liste ==="
|
||||
curl -sf "$BASE" | python -m json.tool
|
||||
|
||||
echo "=== POST création ==="
|
||||
NEW=$(curl -sf -X POST "$BASE" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"bash-test","type":"switch","ip_address":"10.0.60.1","is_active":true}')
|
||||
echo "$NEW" | python -m json.tool
|
||||
ID=$(echo "$NEW" | python -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
|
||||
echo "=== GET #$ID ==="
|
||||
curl -sf "$BASE/$ID" | python -m json.tool
|
||||
|
||||
echo "=== PATCH toggle #$ID ==="
|
||||
curl -sf -X PATCH "$BASE/$ID/toggle" | python -m json.tool
|
||||
|
||||
echo "=== DELETE #$ID ==="
|
||||
curl -sf -X DELETE "$BASE/$ID" -w "HTTP %{http_code}\n"
|
||||
Generated
+304
@@ -0,0 +1,304 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.14"
|
||||
|
||||
[[package]]
|
||||
name = "3-4-api-rest"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "flask" },
|
||||
{ name = "requests" },
|
||||
{ name = "sqlalchemy" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "flask", specifier = ">=3.1.3" },
|
||||
{ name = "requests", specifier = ">=2.34.2" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.51" },
|
||||
]
|
||||
|
||||
[[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 = "certifi"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||
]
|
||||
|
||||
[[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 = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[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 = "greenlet"
|
||||
version = "3.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[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 = "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 = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[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" },
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user