From 3b304bcaa429a601f47f40aeb5fcfc441fc12b8e Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Thu, 8 Oct 2026 10:24:24 +0200 Subject: [PATCH] =?UTF-8?q?Semaine=2015,=20Jour=204=20-=20D=C3=A9but?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Semaine_15/Jour_04/atelier-web-etudiants.zip | Bin 0 -> 25180 bytes .../atelier-web/ENONCES-ATELIER-WEB.md | 509 ++++++++++++++++++ .../atelier-web/FICHE-INSTALLER-PYTHON.md | 213 ++++++++ Semaine_15/Jour_04/atelier-web/MEMO-JOUR-J.md | 139 +++++ .../Jour_04/atelier-web/requirements.txt | 2 + Semaine_15/Jour_04/atelier-web/ruff.toml | 18 + .../atelier-web/starter/.python-version | 1 + .../Jour_04/atelier-web/starter/README.md | 26 + Semaine_15/Jour_04/atelier-web/starter/app.py | 221 ++++++++ .../Jour_04/atelier-web/starter/blocandco.db | Bin 0 -> 16384 bytes .../atelier-web/starter/pyproject.toml | 10 + .../Jour_04/atelier-web/starter/schema.sql | 15 + .../atelier-web/starter/static/style.css | 74 +++ .../starter/templates/accueil.html | 14 + .../atelier-web/starter/templates/admin.html | 30 ++ .../atelier-web/starter/templates/base.html | 40 ++ .../starter/templates/connexion.html | 15 + .../starter/templates/inscription.html | 24 + .../atelier-web/starter/templates/profil.html | 29 + .../Jour_04/atelier-web/starter/test_app.py | 172 ++++++ .../Jour_04/atelier-web/starter/uv.lock | 225 ++++++++ .../atelier-web/starter/verif_poste.py | 79 +++ 22 files changed, 1856 insertions(+) create mode 100644 Semaine_15/Jour_04/atelier-web-etudiants.zip create mode 100644 Semaine_15/Jour_04/atelier-web/ENONCES-ATELIER-WEB.md create mode 100644 Semaine_15/Jour_04/atelier-web/FICHE-INSTALLER-PYTHON.md create mode 100644 Semaine_15/Jour_04/atelier-web/MEMO-JOUR-J.md create mode 100644 Semaine_15/Jour_04/atelier-web/requirements.txt create mode 100644 Semaine_15/Jour_04/atelier-web/ruff.toml create mode 100644 Semaine_15/Jour_04/atelier-web/starter/.python-version create mode 100644 Semaine_15/Jour_04/atelier-web/starter/README.md create mode 100644 Semaine_15/Jour_04/atelier-web/starter/app.py create mode 100644 Semaine_15/Jour_04/atelier-web/starter/blocandco.db create mode 100644 Semaine_15/Jour_04/atelier-web/starter/pyproject.toml create mode 100644 Semaine_15/Jour_04/atelier-web/starter/schema.sql create mode 100644 Semaine_15/Jour_04/atelier-web/starter/static/style.css create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/accueil.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/admin.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/base.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/connexion.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/inscription.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/templates/profil.html create mode 100644 Semaine_15/Jour_04/atelier-web/starter/test_app.py create mode 100644 Semaine_15/Jour_04/atelier-web/starter/uv.lock create mode 100644 Semaine_15/Jour_04/atelier-web/starter/verif_poste.py diff --git a/Semaine_15/Jour_04/atelier-web-etudiants.zip b/Semaine_15/Jour_04/atelier-web-etudiants.zip new file mode 100644 index 0000000000000000000000000000000000000000..4af080983672016f5971c6d02fdd80cbb3936bba GIT binary patch literal 25180 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 mf@Q-BfP40#*00!qkps$dh Le jour de l'épreuve, vous ne saurez qu'en lisant le sujet si un dépôt vous +> est fourni ou si vous partez de rien. **Entraînez-vous aux deux.** + +**A. Page blanche.** Sans rien copier, dans un dossier neuf, créez un projet +Flask qui démarre : environnement virtuel, installation, `app.py`, une route +qui affiche un texte. Chronométrez-vous. + +``` +Objectif : moins de 10 minutes, sans consulter de documentation. +Recommencez demain dans un autre dossier, sans regarder vos notes. +``` + +**B. Projet existant.** Ouvrez le dossier `starter/` et, **avant de coder +quoi que ce soit**, remplissez ce tableau : + +| Question | Votre réponse | +|---|---| +| Combien de routes sont déclarées ? | | +| Où le mot de passe est-il haché ? | | +| Quelles fonctions sont vides ? | | +| Que fait `@app.teardown_appcontext` ? | | +| L'application démarre-t-elle en l'état ? | | + +**Pourquoi cet exercice** : reprendre du code écrit par quelqu'un d'autre est +la situation normale en entreprise, et la moitié des candidats perdent une +heure parce qu'ils ont commencé à taper avant d'avoir lu. + +La fiche `MEMO-JOUR-J.md` récapitule les deux démarches. Gardez-la ouverte +pendant l'épreuve. + +--- + +## Exercice 1 — Votre première page + +**Notions** : route, fonction de vue, serveur de développement. + +Créez un fichier `essai.py` séparé contenant une application Flask minimale +avec une route `/bonjour` qui renvoie le texte `Bonjour Bloc&Co`. +Lancez-la, ouvrez-la dans le navigateur. + +Ajoutez ensuite une route `/bonjour/` qui affiche `Bonjour `. + +``` +http://127.0.0.1:5000/bonjour → Bonjour Bloc&Co +http://127.0.0.1:5000/bonjour/Lea → Bonjour Lea +``` + +**Erreurs attendues** : oublier `app.run()` · deux routes avec le même nom de +fonction → `AssertionError` · modifier le code sans recharger (pensez à +`debug=True`). + +--- + +## Exercice 2 — Séparer le HTML du Python + +**Notions** : templates Jinja, `render_template`, héritage de gabarit. + +Ouvrez `templates/base.html` et `templates/accueil.html`. Répondez par écrit, +en une phrase chacune : + +1. Que fait `{% extends "base.html" %}` ? +2. Où atterrit le contenu de `{% block contenu %}` ? +3. Pourquoi écrit-on `{{ url_for('profil') }}` plutôt que `/profil` ? +4. Dans `base.html`, pourquoi le lien « Administration » n'apparaît-il pas + pour tout le monde ? + +**Pourquoi c'est important** : le jury vous demandera de justifier votre +architecture. « J'ai séparé la présentation de la logique » est une réponse, +encore faut-il pouvoir la montrer dans le code. + +--- + +## Exercice 3 — Un formulaire qui parle au serveur + +**Notions** : méthodes GET et POST, `request.form`. + +Dans `essai.py`, créez une route `/echo` qui accepte GET et POST : + +- en GET, elle affiche un formulaire avec un champ `message` +- en POST, elle réaffiche la page avec le message reçu + +``` +Saisie : "salut" +Affiché : Vous avez écrit : salut +``` + +Puis essayez de saisir `salut`. Que s'affiche-t-il à l'écran ? +Gardez la réponse de côté : on y revient à l'exercice 12. + +**Erreurs attendues** : oublier `methods=["GET", "POST"]` → erreur 405 · +`request.form["message"]` sur un champ absent → `KeyError` (préférez +`request.form.get("message", "")`). + +--- + +# Partie B — Enregistrer les données + +## Exercice 4 — Parler à la base + +**Notions** : SQLite, `SELECT`, `INSERT`, requêtes paramétrées. + +Dans un fichier d'essai, ouvrez `blocandco.db` et : + +1. affichez tous les adhérents +2. affichez uniquement ceux dont le rôle est `admin` +3. comptez le nombre total d'adhérents +4. insérez un adhérent de test, puis supprimez-le + +**Règle non négociable, à appliquer dès maintenant :** une valeur variable ne +s'écrit **jamais** dans le texte de la requête. + +```python +# NON — faille d'injection SQL +db.execute("SELECT * FROM adherents WHERE email = '" + email + "'") + +# OUI — requête paramétrée +db.execute("SELECT * FROM adherents WHERE email = ?", (email,)) +``` + +**Erreurs attendues** : oublier la virgule dans `(email,)` → le tuple n'en est +pas un · oublier `commit()` après un `INSERT` → rien n'est enregistré · +`fetchone()` renvoie `None` quand il n'y a pas de résultat, et le code plante +trois lignes plus loin. + +--- + +## Exercice 5 — L'inscription + +**Notions** : validation des entrées, hachage de mot de passe, contrainte +d'unicité. + +Complétez `valider_inscription()` puis la route `/inscription`. + +**Règles de validation** : + +| Champ | Règle | +|---|---| +| nom, prénom | non vides | +| email | contient `@` et un point après le `@` | +| mot de passe | 12 caractères minimum, une majuscule, un chiffre | +| confirmation | identique au mot de passe | +| email | pas déjà présent en base | + +**Le mot de passe ne doit jamais arriver en clair dans la base.** Utilisez +`generate_password_hash()` de Werkzeug. + +``` +Entrée : Léa / Dupont / lea@test.fr / "Court1A" +Sortie : « Le mot de passe doit faire au moins 12 caractères. » + +Entrée : Léa / Dupont / lea@test.fr / "Grimpe2026ABC" +Sortie : « Compte créé. Vous pouvez vous connecter. » + En base : $scrypt$... (et non "Grimpe2026ABC") +``` + +**Vérification** : `pytest -k inscription` et `pytest -k mot_de_passe` + +**Erreurs attendues** : enregistrer le mot de passe en clair · hacher la +confirmation au lieu du mot de passe · renvoyer le mot de passe dans le +formulaire réaffiché · comparer les e-mails sans normaliser la casse, si bien +que `Lea@Test.fr` et `lea@test.fr` créent deux comptes. + +--- + +# Partie C — Authentification et autorisations + +## Exercice 6 — La connexion + +**Notions** : session, vérification d'empreinte. + +Complétez `utilisateur_courant()` puis la route `/connexion`. + +Le principe : on ne déchiffre **jamais** un mot de passe. On rehache celui qui +est saisi et on compare les deux empreintes, avec `check_password_hash()`. + +**Point de sécurité, et il compte** : que l'adresse existe ou non, que le mot +de passe soit faux ou non, affichez **exactement le même message**. Sinon un +attaquant découvre quelles adresses sont inscrites chez vous — c'est une +énumération de comptes. + +``` +Entrée : lea@test.fr / mauvais → « Identifiants incorrects. » +Entrée : inconnu@test.fr / n'importe → « Identifiants incorrects. » +``` + +**Vérification** : `pytest -k connexion` + +**Erreurs attendues** : messages différents selon le cas · stocker tout +l'utilisateur dans la session au lieu de son seul identifiant · oublier +`session.clear()` avant de connecter quelqu'un (fixation de session) · oublier +`app.secret_key` → Flask refuse d'écrire dans la session. + +--- + +## Exercice 7 — Déconnexion et pages protégées + +**Notions** : décorateur, redirection. + +1. Complétez `/deconnexion` : elle vide la session. +2. Complétez le décorateur `login_requis`. + +Un décorateur est une fonction qui en enveloppe une autre pour ajouter un +contrôle avant son exécution. Ici : « si personne n'est connecté, redirige +vers la connexion ; sinon, laisse passer ». + +``` +Visiteur anonyme sur /profil → redirigé vers /connexion + « Connectez-vous pour accéder à cette page. » +``` + +**Vérification** : `pytest -k protege` et `pytest -k deconnexion` + +**Erreurs attendues** : oublier `@wraps(vue)` → Flask voit deux routes nommées +`wrapper` et refuse de démarrer · placer `@login_requis` **au-dessus** de +`@app.route` (l'ordre compte : la route en premier) · oublier de renvoyer le +résultat de `vue(...)`. + +--- + +## Exercice 8 — L'espace personnel + +**Notions** : `UPDATE`, revalidation côté serveur. + +Complétez `/profil` : afficher les informations, et permettre de modifier nom, +prénom et niveau. + +**Le piège, et c'est le plus important de l'atelier** : le `` · oublier le `WHERE` dans +l'`UPDATE` → tous les adhérents sont modifiés d'un coup · laisser l'utilisateur +changer son propre rôle par ce formulaire. + +--- + +## Exercice 9 — Les rôles + +**Notions** : contrôle d'autorisation, séparation authentification / +autorisation. + +Complétez le décorateur `admin_requis`, la route `/admin` et `/admin/promouvoir`. + +Retenez la distinction, le jury la pose souvent : + +- **authentification** : qui êtes-vous ? (connexion) +- **autorisation** : avez-vous le droit de faire ceci ? (rôle) + +Un utilisateur connecté n'est pas un utilisateur autorisé. + +``` +Visiteur anonyme sur /admin → « Connectez-vous… » +Adhérent connecté sur /admin → « Accès réservé aux administrateurs. » +Administrateur sur /admin → la liste des adhérents +``` + +**Vérification** : `pytest -k admin` et `pytest -k promotion` + +**Erreurs attendues** : masquer le lien dans le menu **sans** protéger la route +(l'URL reste accessible en la tapant) · vérifier le rôle avec `=` au lieu de +`==` · protéger l'affichage mais pas l'action `/admin/promouvoir` — c'est la +faille la plus fréquente et la plus grave. + +--- + +## Exercice 10 — La statistique d'accueil + +**Notions** : `COUNT`, passage de variable au template. + +Affichez sur la page d'accueil le nombre réel d'adhérents inscrits. + +``` +Base vide + compte admin → 1 adhérent inscrit +Après une inscription → 2 adhérents inscrits +``` + +**Vérification** : `pytest -k nombre` + +**Erreurs attendues** : compter en Python avec `len(fetchall())` au lieu d'un +`COUNT(*)` en SQL — correct ici, inacceptable sur un million de lignes · +oublier d'accorder le pluriel. + +--- + +# Partie D — La sécurité, ce que le jury regarde vraiment + +> Les quatre exercices suivants se font **sur votre propre application**. +> C'est légal, c'est le but, et c'est exactement ce qu'on vous demandera de +> démontrer. + +## Exercice 11 — Injection SQL + +**Notions** : OWASP A03, requêtes paramétrées. + +1. Dans une copie de votre code, remplacez volontairement la requête de + connexion par une concaténation : + + ```python + db.execute("SELECT * FROM adherents WHERE email = '" + email + "'") + ``` + +2. Connectez-vous avec l'adresse `lea@test.fr' --` et n'importe quel mot de + passe. Observez. +3. Essayez `' OR '1'='1`. Observez. +4. Revenez à la version paramétrée. Rejouez les deux charges. +5. **Expliquez par écrit, en trois phrases, pourquoi le `?` règle le problème.** + +**Vérification** : `pytest -k injection` + +La réponse attendue : avec `?`, la requête et les données voyagent séparément. +Le moteur a déjà analysé la structure de la requête quand il reçoit la valeur ; +celle-ci ne peut donc plus être lue comme du code SQL. + +--- + +## Exercice 12 — Injection de script (XSS) + +**Notions** : OWASP A03, échappement automatique. + +1. Modifiez votre prénom en `` puis affichez + votre profil. Que se passe-t-il ? Regardez le code source de la page. +2. Dans `profil.html`, remplacez `{{ utilisateur.prenom }}` par + `{{ utilisateur.prenom|safe }}`. Rechargez. +3. Expliquez ce que fait Jinja par défaut, et ce que `|safe` désactive. +4. **Remettez le code dans son état initial.** + +**Vérification** : `pytest -k xss` + +**À retenir** : Jinja échappe automatiquement. Ce filtre `|safe`, vous le +croiserez dans du vrai code — c'est souvent là qu'est la faille. + +--- + +## Exercice 13 — Contrôle d'accès défaillant + +**Notions** : OWASP A01, le risque numéro 1 du classement. + +Trois attaques à mener contre votre propre application : + +1. **Accès direct par l'URL** — déconnectez-vous, tapez `/admin` à la main. + Êtes-vous bloqué ? +2. **Élévation de privilège** — connectez-vous comme simple adhérent, puis + envoyez une requête POST vers `/admin/promouvoir/1`. Votre rôle change-t-il ? + (Le test `test_promotion_interdite_a_un_adherent` fait exactement cela.) +3. **Accès aux données d'autrui** — si vous aviez écrit `/profil/` au lieu + de `/profil`, que se passerait-il en changeant le numéro dans l'URL ? + Expliquez pourquoi la version sans identifiant est plus sûre. + +**Règle** : cacher un bouton n'est pas une mesure de sécurité. Seul le contrôle +exécuté sur le serveur en est une. + +--- + +## Exercice 14 — Les quatre critères, appliqués à votre code + +**Notions** : disponibilité, intégrité, confidentialité, traçabilité. + +Pour chacun des quatre critères, citez **une ligne précise de votre code** qui +y contribue, ou constatez qu'il manque et dites ce que vous ajouteriez. + +| Critère | Dans votre application | +|---|---| +| Confidentialité | ? | +| Intégrité | ? | +| Disponibilité | ? | +| Traçabilité | ? | + +Indice pour le dernier : votre application n'enregistre aujourd'hui **aucune** +trace des connexions. Proposez une table `journal` et dites ce que vous y +mettriez — et surtout ce que vous n'y mettriez **jamais**. + +--- + +# Partie E — L'épreuve + +## Exercice 15 — Projet de synthèse, en autonomie (4 heures) + +**Nouveau cahier des charges, à traiter seul.** + +> **Rouletabosse**, association de covoiturage domicile-travail, veut une +> application web. Elle doit permettre à un salarié de créer un compte +> (nom, prénom, e-mail, mot de passe, entreprise), de se connecter, de +> consulter et modifier son profil. Deux rôles : `membre` et `referent`. +> Seul un référent peut voir la liste des membres et valider un compte. +> La page d'accueil affiche le nombre de membres inscrits. +> Front : HTML / CSS. Back : Python. Mots de passe hachés, sessions +> sécurisées. + +Vous partez d'une page blanche. Vous avez quatre heures. + +**Livrable** : l'application qui tourne en local, plus la présentation de +l'exercice 16. + +**Conseil de méthode** : passez les 20 premières minutes à écrire le schéma de +la base et la liste de vos routes, **avant** la première ligne de code. Les +candidats qui codent immédiatement sont ceux qui ne finissent pas. + +--- + +## Exercice 16 — La présentation au jury + +Une application qui marche mais qu'on ne sait pas présenter est une +application à moitié rendue. Préparez un support de 12 à 15 diapositives : + +1. **Introduction** — le besoin métier, en deux phrases +2. **Schéma de la base de données** — les tables, leurs champs, leurs liens +3. **Modélisation de l'application** — les routes, et le parcours d'un + utilisateur de l'inscription au profil +4. **Choix technologiques** — et leur **justification**. « Flask parce que + c'est léger et que le besoin ne demande pas d'ORM » est une justification. + « Flask parce qu'on l'a vu en cours » n'en est pas une. +5. **Sécurité** — hachage, sessions, requêtes paramétrées, contrôle des rôles, + validation des entrées. Une diapositive par mesure, avec l'extrait de code. +6. **Tests** — votre stratégie, le nombre de tests, une capture de `pytest` +7. **Démonstration** — captures d'écran du parcours complet +8. **Conclusion** — ce qui manque, et ce que vous feriez avec deux jours de plus + +**Préparez une réponse à ces trois questions**, elles tombent presque toujours : + +- Pourquoi hacher plutôt que chiffrer un mot de passe ? +- Que se passe-t-il si je tape l'URL d'administration sans être connecté ? +- Comment savez-vous que votre application fonctionne ? + +--- + +# Grille d'évaluation + +| Compétence du référentiel | Vérifiée par | +|---|---| +| **C15** — procédures d'authentification et d'autorisation intégrées | Exercices 6, 7, 9 | +| **C15** — activités autorisées et interdites définies | Exercices 9, 13 | +| **C15** — gestion sécurisée des données sur tout leur cycle de vie | Exercices 5, 14 | +| **C16** — langages web et logiciel mobilisés (Python, JS…) | Exercices 1 à 10 | +| **C16** — standards de développement respectés | `ruff check .` sur le rendu | +| **C16** — les 4 critères de sécurité respectés | Exercice 14 | +| **C17** — risques identifiés, robustesse aux attaques | Exercices 11, 12, 13 | +| **C17** — composants externes connus et éprouvés | Exercice 16, diapositive 4 | + +| Barème indicatif | Points | +|---|---| +| Partie A — bases web (ex. 1-3) | 2 | +| Partie B — données et inscription (ex. 4-5) | 4 | +| Partie C — authentification et rôles (ex. 6-10) | 6 | +| Partie D — sécurité (ex. 11-14) | 4 | +| Partie E — présentation (ex. 16) | 4 | + +**Retraits transversaux** : mot de passe en clair en base (**−5, éliminatoire**) +· requête SQL construite par concaténation (−3) · route protégée uniquement par +le masquage d'un lien (−3) · `except:` nu (−1) · `app.secret_key` codée en dur +dans un rendu présenté comme « prêt pour la production » (−1). diff --git a/Semaine_15/Jour_04/atelier-web/FICHE-INSTALLER-PYTHON.md b/Semaine_15/Jour_04/atelier-web/FICHE-INSTALLER-PYTHON.md new file mode 100644 index 0000000..dc4194d --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/FICHE-INSTALLER-PYTHON.md @@ -0,0 +1,213 @@ +# Fiche — installer Python et Flask + +À garder de côté. Vous n'en avez besoin que si `verif_poste.py` affiche une +ligne `[ KO ]`, ou si votre terminal ne reconnaît pas la commande `python`. + +**Durée : 10 minutes.** Suivez les étapes dans l'ordre, sans en sauter. + +--- + +# Windows + +## Étape 1 — Vérifier ce que vous avez déjà + +Ouvrez un terminal : touche **Windows**, tapez `cmd`, **Entrée**. + +Tapez : + +``` +py --list +``` + +| Ce que vous voyez | Ce que ça veut dire | +|---|---| +| Une liste avec `-V:3.9` ou mieux | **Python est déjà installé.** Passez directement à l'étape 4. | +| `'py' n'est pas reconnu…` | Python n'est pas installé, ou mal installé. Étape 2. | +| Uniquement des versions `3.8` ou moins | Trop ancien pour Flask 3. Étape 2. | + +--- + +## Étape 2 — Télécharger + +Allez sur **https://www.python.org/downloads/** + +Le gros bouton jaune propose la dernière version pour votre système. +Cliquez, puis lancez le fichier téléchargé. + +> N'installez pas Python depuis le Microsoft Store. Il fonctionne, mais il +> isole les fichiers d'une façon qui pose des problèmes avec les +> environnements virtuels — exactement ce qu'on va utiliser. + +--- + +## Étape 3 — Installer : l'écran qui compte + +Le premier écran de l'installateur contient **une case à cocher en bas** : + +``` + ☑ Add python.exe to PATH ← COCHEZ-LA +``` + +**C'est l'étape la plus importante de toute la fiche.** Si vous l'oubliez, +Windows ne saura pas où trouver Python et toutes les commandes échoueront. + +Cochez la case, puis cliquez sur **Install Now**. + +Si vous avez déjà cliqué sans cocher : relancez l'installateur, choisissez +**Modify**, puis **Repair** — ou désinstallez et recommencez. + +À la fin, s'il propose **« Disable path length limit »**, acceptez. + +--- + +## Étape 4 — Vérifier + +**Fermez complètement votre terminal et rouvrez-en un neuf.** Le PATH n'est lu +qu'à l'ouverture : sans ça, vous testerez l'ancienne configuration. + +``` +py --version +``` + +Vous devez voir un numéro de version, par exemple `Python 3.13.2`. + +--- + +## Étape 5 — Installer Flask + +``` +py -m pip install flask pytest +``` + +Puis relancez la vérification : + +``` +py verif_poste.py +``` + +Vous devez obtenir « Tout est prêt. Rien à faire. » + +--- + +# macOS + +## Étape 1 — Vérifier + +Ouvrez **Terminal** (Spotlight : `Cmd + Espace`, tapez `terminal`). + +``` +python3 --version +``` + +Si vous obtenez 3.9 ou mieux, passez à l'étape 3. + +## Étape 2 — Installer + +Téléchargez l'installateur macOS sur **https://www.python.org/downloads/** +et suivez-le. Aucune case particulière à cocher : sur macOS le PATH est +configuré automatiquement. + +## Étape 3 — Installer Flask + +``` +python3 -m pip install flask pytest +``` + +Sur macOS et Linux, remplacez partout `py` par `python3` dans les commandes +de l'atelier. + +--- + +# Dépannage + +## « 'python' n'est pas reconnu en tant que commande interne » + +La case **Add python.exe to PATH** n'a pas été cochée. + +Solution immédiate : utilisez `py` à la place de `python`. Le lanceur `py` +est installé séparément et fonctionne presque toujours. + +Solution durable : relancez l'installateur → **Modify** → **Repair**. + +--- + +## Taper `python` ouvre le Microsoft Store + +Windows intercepte la commande avec un raccourci publicitaire. + +**Paramètres** → **Applications** → **Paramètres avancés des applications** +→ **Alias d'exécution d'application** → désactivez les deux lignes +`python.exe` et `python3.exe`. + +En attendant, utilisez `py` : il n'est pas concerné. + +--- + +## J'ai plusieurs versions de Python et je ne sais pas laquelle tourne + +``` +py --list +``` + +Vous choisissez explicitement laquelle utiliser : + +``` +py -3.13 verif_poste.py # remplacez 3.13 par VOTRE version +py -3.13 -m pip install flask +``` + +Le numéro après `-` est la version. Utilisez **toujours le même** pendant +tout l'atelier, sinon vous installerez Flask dans une version et lancerez le +code avec une autre — c'est la cause numéro un des `ModuleNotFoundError`. + +--- + +## `pip` échoue avec une erreur de certificat ou de proxy + +Vous êtes probablement sur un réseau d'entreprise ou d'école qui filtre. + +Essayez d'abord : + +``` +py -m pip install --user flask pytest +``` + +Si cela échoue encore, utilisez votre connexion personnelle (partage de +connexion du téléphone) le temps de l'installation. Une fois Flask installé, +le réseau n'a plus d'importance. + +--- + +## `ModuleNotFoundError: No module named 'flask'` + +Flask est installé, mais pas pour l'interpréteur qui exécute votre code. + +Vérifiez avec quel Python vous travaillez : + +``` +py verif_poste.py +``` + +La ligne **Executable** vous donne le chemin exact. Installez Flask pour +celui-là précisément : + +``` +py -m pip install flask +``` + +Si vous utilisez un environnement virtuel, vérifiez qu'il est **activé** : +votre ligne de commande doit commencer par `(.venv)`. + +--- + +## Rien ne fonctionne et le cours a commencé + +**Ne restez pas bloqué seul.** Dans le chat du cours, envoyez : + +1. Le résultat complet de `py verif_poste.py` +2. La commande que vous avez tapée +3. Le message d'erreur **en entier** + +En attendant la réponse, suivez sur l'écran partagé et binômez avec un +camarade. Vous rattraperez le code à la pause : ce qui compte maintenant, +c'est de ne pas perdre le fil du raisonnement. diff --git a/Semaine_15/Jour_04/atelier-web/MEMO-JOUR-J.md b/Semaine_15/Jour_04/atelier-web/MEMO-JOUR-J.md new file mode 100644 index 0000000..403dfe7 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/MEMO-JOUR-J.md @@ -0,0 +1,139 @@ +# Mémo — les 30 premières minutes de l'épreuve + +> À garder sous les yeux pendant l'examen. +> Vous avez 4 heures. Les 30 premières décident des 3 h 30 suivantes. + +Deux situations possibles. Lisez le sujet : on vous donne un dépôt, ou pas. + +--- + +## Situation A — on vous fournit un dépôt + +**Ne codez rien avant d'avoir fait ces six étapes.** + +1. **Clonez**, puis **lisez le README en entier.** Cinq minutes de lecture + valent une heure de tâtonnement. Il dit comment lancer le projet. + +2. **Cherchez le fichier de dépendances** : `requirements.txt`, + `pyproject.toml` ou `Pipfile`. Il vous dit quel framework est imposé. + +3. **Installez et lancez l'application telle quelle**, avant toute + modification. Si elle ne démarre pas, c'est votre premier problème à + résoudre — et vous devez le savoir maintenant, pas à 15 h. + +4. **Cartographiez le projet en cinq minutes**, par écrit : + + | Question | Réponse | + |---|---| + | Où sont déclarées les routes ? | | + | Où sont les templates ? | | + | Y a-t-il déjà une base de données ? un schéma ? | | + | Qu'est-ce qui marche déjà ? | | + | Qu'est-ce qui est vide ou marqué « à faire » ? | | + +5. **Repérez les conventions existantes** : langue des noms de variables, + structure des fonctions, façon d'accéder à la base. Votre code doit + ressembler à celui qui est déjà là. Un jury le remarque. + +6. **Faites un premier commit** avant de commencer. Vous aurez un point de + retour si vous cassez tout. + +--- + +## Situation B — page blanche + +**L'arborescence à créer, dans cet ordre :** + +``` +monprojet/ + app.py + schema.sql + requirements.txt + templates/ + base.html + static/ + style.css +``` + +**Les commandes (Windows)** : + +``` +mkdir monprojet +cd monprojet +py -m venv .venv +.venv\Scripts\activate +pip install flask +pip freeze > requirements.txt +``` + +Sur macOS ou Linux, remplacez les deux premières lignes par : +`python3 -m venv .venv` puis `source .venv/bin/activate` + +**Le `app.py` minimal, à taper de mémoire :** + +```python +from flask import Flask + +app = Flask(__name__) +app.secret_key = "a-changer" + +@app.route("/") +def accueil(): + return "ça tourne" + +if __name__ == "__main__": + app.run(debug=True) +``` + +Lancez-le. **Vous devez voir « ça tourne » dans le navigateur avant d'écrire +la moindre fonctionnalité.** Tant que ce n'est pas le cas, rien d'autre +n'a d'importance. + +--- + +## Dans les deux cas : l'ordre de construction + +Ne construisez jamais dans un autre ordre. Chaque étape s'appuie sur la +précédente, et chacune doit marcher avant de passer à la suivante. + +| # | Étape | Fini quand… | +|---|---|---| +| 1 | L'application démarre | une page s'affiche | +| 2 | La base et la table existent | vous lisez une ligne en Python | +| 3 | L'inscription | un compte apparaît en base, mot de passe **haché** | +| 4 | La connexion | la session retient qui vous êtes | +| 5 | Le profil | vous voyez et modifiez vos infos | +| 6 | Les rôles | un non-admin est refusé sur la page admin | +| 7 | La statistique | le compteur s'affiche sur l'accueil | +| 8 | **Les captures d'écran** | vous avez de quoi faire la démo | + +**À 14 h, arrêtez de coder.** La présentation vaut autant que le code. Une +application à 80 % bien présentée passe mieux qu'une application à 100 % +sans support. + +--- + +## Les pièges qui coûtent des heures + +| Symptôme | Cause réelle | +|---|---| +| `ModuleNotFoundError: flask` | environnement virtuel non activé — regardez si `(.venv)` est au début de la ligne | +| `Address already in use` | une ancienne instance tourne encore : fermez-la, ou `app.run(port=5001)` | +| Les modifications n'apparaissent pas | `debug=True` oublié, ou page en cache (Ctrl+F5) | +| `jinja2.TemplateNotFound` | le dossier doit s'appeler exactement `templates` | +| `RuntimeError: The session is unavailable` | `app.secret_key` manquante | +| Les accents s'affichent en `?` | `encoding="utf-8"` oublié à la lecture du fichier | +| L'erreur 405 | la route n'accepte pas POST : `methods=["GET", "POST"]` | + +--- + +## Ce qui est éliminatoire + +Quoi qu'il arrive, même à court de temps : + +- **Jamais** de mot de passe en clair dans la base +- **Jamais** de requête SQL construite avec `+` ou une f-string +- **Toujours** le contrôle de rôle sur le serveur, pas seulement un lien caché + +Si vous devez sacrifier une fonctionnalité, sacrifiez-la. Ne sacrifiez +jamais une de ces trois règles. diff --git a/Semaine_15/Jour_04/atelier-web/requirements.txt b/Semaine_15/Jour_04/atelier-web/requirements.txt new file mode 100644 index 0000000..258ff8f --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/requirements.txt @@ -0,0 +1,2 @@ +Flask>=3.0 +pytest>=8.0 diff --git a/Semaine_15/Jour_04/atelier-web/ruff.toml b/Semaine_15/Jour_04/atelier-web/ruff.toml new file mode 100644 index 0000000..d4ff93a --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/ruff.toml @@ -0,0 +1,18 @@ +# Règles de style de l'atelier. +# Les étudiants lancent ruff check . avant de rendre. +# Ce fichier garantit que tout le monde obtient le même résultat. + +line-length = 88 + +[lint] +select = ["E", "F", "W", "I", "UP", "B", "RET"] + +ignore = [ + # On enseigne date.today(), les fuseaux horaires ne sont pas au programme. + "DTZ011", +] + +[lint.per-file-ignores] +# Dans le squelette, ces imports sont fournis d'avance aux étudiants : +# ils deviennent utiles au fur et à mesure des exercices. +"starter/app.py" = ["F401", "RET504", "RET501"] diff --git a/Semaine_15/Jour_04/atelier-web/starter/.python-version b/Semaine_15/Jour_04/atelier-web/starter/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Semaine_15/Jour_04/atelier-web/starter/README.md b/Semaine_15/Jour_04/atelier-web/starter/README.md new file mode 100644 index 0000000..50cb126 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/README.md @@ -0,0 +1,26 @@ +# Bloc&Co — squelette de l'atelier + +## Démarrer + + py -m venv .venv + .venv\Scripts\activate + pip install -r ..\requirements.txt + python app.py + +Sur macOS ou Linux, remplacez les deux premières lignes par : +`python3 -m venv .venv` puis `source .venv/bin/activate` + +Puis ouvrir http://127.0.0.1:5000 + +## Ce qui est fourni + +- `schema.sql` — la table `adherents`, à ne pas modifier +- `templates/` — tout le HTML, déjà écrit +- `static/style.css` — la feuille de style +- `app.py` — la plomberie de la base de données, et des fonctions à compléter +- `verif_poste.py` — vérifie que votre poste est prêt (à lancer en premier) + +## Ce que vous écrivez + +Tout ce qui est marqué `# À FAIRE`. Suivez les exercices dans l'ordre : +l'application ne fonctionnera entièrement qu'à la fin de l'exercice 10. diff --git a/Semaine_15/Jour_04/atelier-web/starter/app.py b/Semaine_15/Jour_04/atelier-web/starter/app.py new file mode 100644 index 0000000..4fa72be --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/app.py @@ -0,0 +1,221 @@ +"""Bloc&Co — gestion des adhérents d'une salle d'escalade. + +Squelette de l'atelier. Complétez tout ce qui est marqué « À FAIRE ». +Lancement : python app.py puis http://127.0.0.1:5000 +""" + +import sqlite3 +from datetime import date +from functools import wraps +from pathlib import Path + +from flask import Flask, flash, g, redirect, render_template, request, session, url_for +from werkzeug.security import check_password_hash, generate_password_hash + +BASE = Path(__file__).parent +DB = BASE / "blocandco.db" + +app = Flask(__name__) +app.secret_key = "cle-de-developpement-a-changer" + +levelX = ["debutant", "intermediaire", "confirme"] + + +# ========================================================================== +# FOURNI — plomberie de la base de données. Ne rien modifier ici. +# ========================================================================== + +def get_db(): + """Une connexion par requête, rangée dans g et fermée automatiquement.""" + if "db" not in g: + g.db = sqlite3.connect(DB) + g.db.row_factory = sqlite3.Row + return g.db + + +@app.teardown_appcontext +def close_db(exception): + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_db(): + """Crée la base au premier lancement et sème un compte administrateur.""" + if DB.exists(): + return + con = sqlite3.connect(DB) + con.executescript((BASE / "schema.sql").read_text(encoding="utf-8")) + con.execute( + "INSERT INTO adherents (lastname, firstname, email, password, level, role," + " sub_date) VALUES (?, ?, ?, ?, ?, ?, ?)", + ("Martin", "Claire", "admin@blocandco.fr", + generate_password_hash("Admin2026!"), "confirme", "admin", + date.today().isoformat()), + ) + con.commit() + con.close() + print("Base créée. Compte admin : admin@blocandco.fr / Admin2026!") + + +@app.context_processor +def injecter_utilisateur(): + """Rend la variable `utilisateur` disponible dans tous les templates.""" + return {"utilisateur": utilisateur_courant()} + + +# ========================================================================== +# EXERCICE 6 — qui est connecté ? +# ========================================================================== + +def utilisateur_courant(): + """Renvoie la ligne de l'adhérent connecté, ou None si personne. + + À FAIRE : + - si "user_id" n'est pas dans `session`, renvoyer None + - sinon, aller chercher la ligne correspondante dans la base + """ + return None + + +# ========================================================================== +# EXERCICE 7 — protéger une page +# ========================================================================== + +def login_requis(vue): + """Interdit l'accès à la vue si personne n'est connecté. + + À FAIRE : si `utilisateur_courant()` vaut None, afficher un message + et rediriger vers la page de connexion. Sinon, appeler la vue. + """ + @wraps(vue) + def wrapper(*args, **kwargs): + return vue(*args, **kwargs) + return wrapper + + +# ========================================================================== +# EXERCICE 9 — réserver une page aux administrateurs +# ========================================================================== + +def admin_requis(vue): + """Interdit l'accès à la vue si l'utilisateur n'est pas administrateur. + + À FAIRE : refuser les visiteurs non connectés, PUIS refuser ceux dont + le rôle n'est pas "admin". Deux messages différents. + """ + @wraps(vue) + def wrapper(*args, **kwargs): + return vue(*args, **kwargs) + return wrapper + + +# ========================================================================== +# EXERCICE 5 — valider le formulaire d'inscription +# ========================================================================== + +def valider_inscription(lastname, firstname, email, mdp, confirmation): + """Renvoie la liste des messages d'erreur. Liste vide = tout est bon. + + À FAIRE, dans cet ordre : + - lastname et prélastname non vides + - email contenant "@" et un point après le "@" + - mot de passe d'au moins 12 caractères + - au moins un chiffre, au moins une majuscule + - les deux mots de passe identiques + """ + erreurs = [] + return erreurs + + +# ========================================================================== +# ROUTES +# ========================================================================== + +@app.route("/") +def accueil(): + """EXERCICE 10 — afficher le lastnamebre d'adhérents inscrits. + + À FAIRE : remplacer le 0 par le vrai total, lu dans la base + avec un SELECT COUNT(*). + """ + nb = 0 + return render_template("accueil.html", nb_adherents=nb) + + +@app.route("/inscription", methods=["GET", "POST"]) +def inscription(): + """EXERCICE 5 — créer un compte. + + À FAIRE : + - en POST, récupérer les champs du formulaire + - les valider avec valider_inscription() + - refuser une adresse e-mail déjà présente dans la base + - hacher le mot de passe avec generate_password_hash() + - insérer l'adhérent avec le rôle "adherent" + - rediriger vers la page de connexion + """ + if request.method == "POST": + pass + return render_template("inscription.html") + + +@app.route("/connexion", methods=["GET", "POST"]) +def connexion(): + """EXERCICE 6 — se connecter. + + À FAIRE : + - chercher l'adhérent par son e-mail + - vérifier le mot de passe avec check_password_hash() + - en cas d'échec, afficher LE MÊME message que l'adresse existe ou non + - en cas de succès, vider la session puis y stocker l'identifiant + """ + if request.method == "POST": + pass + return render_template("connexion.html") + + +@app.route("/deconnexion") +def deconnexion(): + """EXERCICE 7 — se déconnecter. + + À FAIRE : vider la session et rediriger vers l'accueil. + """ + return redirect(url_for("accueil")) + + +@app.route("/profil", methods=["GET", "POST"]) +@login_requis +def profil(): + """EXERCICE 8 — consulter et modifier son profil. + + À FAIRE : + - en POST, mettre à jour lastname, prélastname et level de l'utilisateur CONNECTÉ + - refuser un level qui n'est pas dans levelX + """ + if request.method == "POST": + pass + return render_template("profil.html", levelx=levelX) + + +@app.route("/admin") +@admin_requis +def admin(): + """EXERCICE 9 — lister les adhérents. + + À FAIRE : lire tous les adhérents, les plus récents d'abord. + """ + adherents = [] + return render_template("admin.html", adherents=adherents) + + +@app.route("/admin/promouvoir/", methods=["POST"]) +@admin_requis +def promouvoir(adherent_id): + """EXERCICE 9 (suite) — promouvoir un adhérent administrateur.""" + return redirect(url_for("admin")) + + +if __name__ == "__main__": + init_db() + app.run(debug=True) diff --git a/Semaine_15/Jour_04/atelier-web/starter/blocandco.db b/Semaine_15/Jour_04/atelier-web/starter/blocandco.db new file mode 100644 index 0000000000000000000000000000000000000000..d9e815d5f938e80b96e111a00247a33a4340a153 GIT binary patch literal 16384 zcmeI(O>Wab6ae6HQne5fg$;@-fig&y+Nw$A|F~T>6ax`y+6G4=2q_wS#_CkDo!U-I zm%xGxz>!#R5f&`FV#(yUX$l9Zcu#gRw&y+1eD=nU^5(d~Q-=L04oHeM^bkoB+QS$j zlo9KaSf?S0#^Uz$*uTCrX#3lbeBn3B=D(u+&%$@{1pxve00JNY0w4eaAOHd&00JN| z5je^&7q_;gPD@1>Pu zsU&?_8Pz28gvBgOlUXBkqt0wM-pFAQoDM(dqTY1umv##uwdx18RtLYfJGj<%4(m-( z=fG|{DX_b;hyH9gPZL}W8AGT#&VTitSs+Hi!<3crDG?`1aF1b815<`aHO3R-=Zp-l6K_ojKeu1h z+6@Ph1< zABmJhtEz6AzQLw8!wG7j(`Z^`L>w8R7UB#nR zoWzSlpU1&??5eDq6-BPdR!&@5sqhPl1p)*>00ck)1V8`;KmY_l00ck)1VG@<3p~my z_iw+r82|o%g$h@9eu6Lo5C8!X009sH0T2KI5C8!X009sHftkPqDO(aR0w(_e`2$aj B2Xg=b literal 0 HcmV?d00001 diff --git a/Semaine_15/Jour_04/atelier-web/starter/pyproject.toml b/Semaine_15/Jour_04/atelier-web/starter/pyproject.toml new file mode 100644 index 0000000..fd3f721 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "starter" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "flask>=3.1.3", + "pytest>=9.1.1", +] diff --git a/Semaine_15/Jour_04/atelier-web/starter/schema.sql b/Semaine_15/Jour_04/atelier-web/starter/schema.sql new file mode 100644 index 0000000..dc6fde8 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/schema.sql @@ -0,0 +1,15 @@ +-- Schéma de la base « Bloc&Co » +-- Exécuté une seule fois, au premier démarrage de l'application. + +DROP TABLE IF EXISTS adherents; + +CREATE TABLE adherents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + lastname TEXT NOT NULL, + firstname TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, -- empreinte, jamais le mot de passe + level TEXT NOT NULL DEFAULT 'debutant', + role TEXT NOT NULL DEFAULT 'adherent', + sub_date TEXT NOT NULL +); diff --git a/Semaine_15/Jour_04/atelier-web/starter/static/style.css b/Semaine_15/Jour_04/atelier-web/starter/static/style.css new file mode 100644 index 0000000..2c98700 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/static/style.css @@ -0,0 +1,74 @@ +:root { + --bleu: #0b6e9e; + --bleu-fonce: #084f71; + --gris: #5c6672; + --bord: #dde3e9; + --fond: #f6f8fa; + --rouge: #a8341f; + --vert: #1f6b45; +} +* { box-sizing: border-box; } +body { + margin: 0; + font-family: "Segoe UI", system-ui, sans-serif; + color: #1a1f26; + background: var(--fond); + line-height: 1.55; +} +header { + display: flex; align-items: center; gap: 1.5rem; + padding: 0.9rem 1.5rem; background: #fff; + border-bottom: 1px solid var(--bord); +} +.logo { font-weight: 700; font-size: 1.15rem; color: var(--bleu); text-decoration: none; } +nav { margin-left: auto; display: flex; gap: 1.1rem; align-items: center; } +nav a { color: var(--gris); text-decoration: none; font-size: 0.92rem; } +nav a:hover { color: var(--bleu); } +main { max-width: 780px; margin: 0 auto; padding: 2rem 1.5rem 4rem; } +h1 { font-size: 1.7rem; margin: 0 0 0.4rem; } +h2 { font-size: 1.15rem; margin: 2rem 0 0.6rem; } +.chapeau { color: var(--gris); margin-top: 0; } + +.carte { + background: #fff; border: 1px solid var(--bord); border-radius: 8px; + padding: 1.25rem; display: flex; flex-direction: column; gap: 0.85rem; + max-width: 420px; +} +label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.88rem; color: var(--gris); } +input, select { + padding: 0.5rem 0.65rem; border: 1px solid var(--bord); border-radius: 5px; + font-size: 0.95rem; font-family: inherit; color: #1a1f26; background: #fff; +} +input:focus, select:focus { outline: 2px solid var(--bleu); outline-offset: 1px; border-color: var(--bleu); } +.aide { font-size: 0.8rem; color: var(--gris); margin: 0; } + +button, .bouton { + background: var(--bleu); color: #fff; border: none; border-radius: 5px; + padding: 0.55rem 1rem; font-size: 0.95rem; font-family: inherit; + cursor: pointer; text-decoration: none; display: inline-block; text-align: center; +} +button:hover, .bouton:hover { background: var(--bleu-fonce); } +button.lien { background: none; color: var(--bleu); padding: 0; text-decoration: underline; } +button.lien:hover { background: none; color: var(--bleu-fonce); } + +.messages { list-style: none; padding: 0; margin: 0 0 1.25rem; display: flex; flex-direction: column; gap: 0.4rem; } +.messages li { padding: 0.6rem 0.85rem; border-radius: 5px; font-size: 0.9rem; border-left: 3px solid; } +.messages .erreur { background: #fbecea; border-color: var(--rouge); color: var(--rouge); } +.messages .succes { background: #e9f3ed; border-color: var(--vert); color: var(--vert); } + +.stat { + display: flex; align-items: baseline; gap: 0.6rem; + background: #fff; border: 1px solid var(--bord); border-radius: 8px; + padding: 1.1rem 1.4rem; margin: 1.5rem 0; max-width: 420px; +} +.chiffre { font-size: 2.4rem; font-weight: 700; color: var(--bleu); line-height: 1; } +.libelle { color: var(--gris); } + +.infos { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1.5rem; } +.infos dt { color: var(--gris); font-size: 0.88rem; } +.infos dd { margin: 0; } + +table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid var(--bord); border-radius: 8px; overflow: hidden; font-size: 0.9rem; } +th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--bord); } +th { background: #eef2f6; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gris); } +tr:last-child td { border-bottom: none; } diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/accueil.html b/Semaine_15/Jour_04/atelier-web/starter/templates/accueil.html new file mode 100644 index 0000000..7968ccb --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/accueil.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block contenu %} +

Salle d'escalade Bloc&Co

+

Blocs, voies et cours collectifs, sept jours sur sept.

+ +
+ {{ nb_adherents }} + adhérent{{ 's' if nb_adherents > 1 else '' }} inscrit{{ 's' if nb_adherents > 1 else '' }} +
+ + {% if not utilisateur %} +

Créer mon compte

+ {% endif %} +{% endblock %} diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/admin.html b/Semaine_15/Jour_04/atelier-web/starter/templates/admin.html new file mode 100644 index 0000000..f6c4257 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/admin.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block titre %}Administration — Bloc&Co{% endblock %} +{% block contenu %} +

Administration

+

{{ adherents|length }} adhérent{{ 's' if adherents|length > 1 else '' }} enregistré{{ 's' if adherents|length > 1 else '' }}.

+ + + + + + + {% for a in adherents %} + + + + + + + + + {% endfor %} + +
lastnameE-maillevelRôleInscription
{{ a.firstname }} {{ a.lastname }}{{ a.email }}{{ a.level }}{{ a.role }}{{ a.sub_date }} + {% if a.role != 'admin' %} +
+ +
+ {% endif %} +
+{% endblock %} diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/base.html b/Semaine_15/Jour_04/atelier-web/starter/templates/base.html new file mode 100644 index 0000000..b4de430 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/base.html @@ -0,0 +1,40 @@ + + + + + + {% block titre %}Bloc&Co{% endblock %} + + + +
+ + +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for categorie, texte in messages %} +
  • {{ texte }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block contenu %}{% endblock %} +
+ + diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/connexion.html b/Semaine_15/Jour_04/atelier-web/starter/templates/connexion.html new file mode 100644 index 0000000..0f3827c --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/connexion.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block titre %}Connexion — Bloc&Co{% endblock %} +{% block contenu %} +

Connexion

+
+ + + +
+

Pas encore de compte ? Inscrivez-vous.

+{% endblock %} diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/inscription.html b/Semaine_15/Jour_04/atelier-web/starter/templates/inscription.html new file mode 100644 index 0000000..5255942 --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/inscription.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block titre %}Inscription — Bloc&Co{% endblock %} +{% block contenu %} +

Créer un compte

+
+ + + + + +

12 caractères minimum, au moins une majuscule et un chiffre.

+ +
+{% endblock %} diff --git a/Semaine_15/Jour_04/atelier-web/starter/templates/profil.html b/Semaine_15/Jour_04/atelier-web/starter/templates/profil.html new file mode 100644 index 0000000..4c2130a --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/templates/profil.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block titre %}Mon profil — Bloc&Co{% endblock %} +{% block contenu %} +

Mon profil

+ +
+
Adresse e-mail
{{ utilisateur.email }}
+
Rôle
{{ utilisateur.role }}
+
Inscrit depuis le
{{ utilisateur.sub_date }}
+
+ +

Modifier mes informations

+
+ + + + +
+{% endblock %} diff --git a/Semaine_15/Jour_04/atelier-web/starter/test_app.py b/Semaine_15/Jour_04/atelier-web/starter/test_app.py new file mode 100644 index 0000000..147c4ba --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/test_app.py @@ -0,0 +1,172 @@ +"""Tests de l'application Bloc&Co. + +Lancement : pytest -v + +Ces tests valident le comportement attendu à la fin de l'atelier. +Donnez-les aux étudiants : un exercice est réussi quand ses tests passent. +""" + +import sqlite3 + +import app as application +import pytest + +@pytest.fixture() +def client(tmp_path, monkeypatch): + """Une application neuve, avec une base jetable, pour chaque test.""" + base = tmp_path / "test.db" + monkeypatch.setattr(application, "DB", base) + application.init_db() + application.app.config["TESTING"] = True + with application.app.test_client() as c: + yield c + +def subscribe(client, email="lea@test.fr", mdp="Grimpe2026ABC"): + return client.post("/inscription", data={ + "lastname": "Dupont", "firstname": "Lea", "email": email, + "password": mdp, "confirmation": mdp, + }, follow_redirects=True) + +def connect(client, email="lea@test.fr", mdp="Grimpe2026ABC"): + return client.post("/connexion", data={ + "email": email, "password": mdp, + }, follow_redirects=True) + +# --- Exercice 10 : la statistique ---------------------------------------- + +def test_accueil_affiche_le_lastnamebre_d_adherents(client): + reponse = client.get("/") + assert reponse.status_code == 200 + assert b">1<" in reponse.data # le compte admin seul + + subscribe(client) + assert b">2<" in client.get("/").data + +# --- Exercice 5 : l'inscription ------------------------------------------ + +def test_inscription_password_trop_court_refuse(client): + reponse = subscribe(client, mdp="Court1A") + assert b"12 caract" in reponse.data + +def test_inscription_password_sans_chiffre_refuse(client): + reponse = subscribe(client, mdp="MotDePasseSansChiffre") + assert b"chiffre" in reponse.data + +def test_inscription_email_en_double_refuse(client): + subscribe(client) + reponse = subscribe(client) + assert "déjà utilis".encode() in reponse.data + +def test_inscription_email_insensible_a_la_casse(client): + subscribe(client, email="Lea@Test.fr") + reponse = subscribe(client, email="lea@test.fr") + assert "déjà utilis".encode() in reponse.data + +def test_inscription_password_jamais_en_clair(client): + subscribe(client) + stocke = sqlite3.connect(application.DB).execute( + "SELECT password FROM adherents WHERE email = 'lea@test.fr'" + ).fetchone()[0] + assert stocke != "Grimpe2026ABC" + assert len(stocke) > 40 + +# --- Exercice 6 : la connexion ------------------------------------------- + +def test_mauvais_password_refuse(client): + subscribe(client) + reponse = connect(client, mdp="MauvaisMotDePasse") + assert b"Identifiants incorrects" in reponse.data + +def test_compte_inexistant_donne_le_meme_message(client): + """Le message ne doit pas révéler si l'adresse existe dans la base.""" + subscribe(client) + inconnu = connect(client, email="personne@test.fr", mdp="Grimpe2026ABC") + mauvais = connect(client, mdp="MauvaisMotDePasse") + assert b"Identifiants incorrects" in inconnu.data + assert b"Identifiants incorrects" in mauvais.data + +def test_connexion_reussie(client): + subscribe(client) + reponse = connect(client) + assert b"Mon profil" in reponse.data + +# --- Exercice 7 : les pages protégées ------------------------------------ + +def test_profil_protege_sans_connexion(client): + reponse = client.get("/profil", follow_redirects=True) + assert b"Connectez-vous" in reponse.data + +def test_profil_protege_apres_deconnexion(client): + subscribe(client) + connect(client) + client.get("/deconnexion") + reponse = client.get("/profil", follow_redirects=True) + assert b"Connectez-vous" in reponse.data + +# --- Exercice 8 : le profil ---------------------------------------------- + +def test_profil_modifiable(client): + subscribe(client) + connect(client) + reponse = client.post("/profil", data={ + "lastname": "Dupont", "firstname": "Léa", "level": "confirme", + }, follow_redirects=True) + assert "mis à jour".encode() in reponse.data + +def test_level_invalide_rejete(client): + """Le formulaire HTML est modifiable : le serveur doit revalider.""" + subscribe(client) + connect(client) + reponse = client.post("/profil", data={ + "lastname": "Dupont", "firstname": "Lea", "level": "super_admin", + }, follow_redirects=True) + assert b"level invalide" in reponse.data + +# --- Exercice 9 : les rôles ---------------------------------------------- + +def test_admin_interdit_a_un_adherent(client): + subscribe(client) + connect(client) + reponse = client.get("/admin", follow_redirects=True) + assert "réservé aux administrateurs".encode() in reponse.data + +def test_admin_accessible_a_l_administrateur(client): + subscribe(client) + connect(client, email="admin@blocandco.fr", mdp="Admin2026!") + reponse = client.get("/admin") + assert reponse.status_code == 200 + assert b"lea@test.fr" in reponse.data + +def test_promotion_interdite_a_un_adherent(client): + subscribe(client) + connect(client) + client.post("/admin/promouvoir/1", follow_redirects=True) + role = sqlite3.connect(application.DB).execute( + "SELECT role FROM adherents WHERE email = 'lea@test.fr'" + ).fetchone()[0] + assert role == "adherent" + +# --- Exercice 11 : injection SQL ----------------------------------------- + +def test_injection_sql_sur_la_connexion(client): + """Avec des requêtes paramétrées, cette charge ne doit rien ouvrir.""" + subscribe(client) + reponse = client.post("/connexion", data={ + "email": "lea@test.fr' OR '1'='1", + "password": "peu importe", + }, follow_redirects=True) + assert b"Identifiants incorrects" in reponse.data + assert b"Mon profil" not in reponse.data + +# --- Exercice 12 : XSS --------------------------------------------------- + +def test_xss_dans_le_firstname_est_echappe(client): + subscribe(client) + connect(client) + client.post("/profil", data={ + "lastname": "Dupont", "firstname": "", + "level": "debutant", + }, follow_redirects=True) + reponse = client.get("/profil") + assert b"" not in reponse.data + assert b"<script>" in reponse.data diff --git a/Semaine_15/Jour_04/atelier-web/starter/uv.lock b/Semaine_15/Jour_04/atelier-web/starter/uv.lock new file mode 100644 index 0000000..16c518c --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/uv.lock @@ -0,0 +1,225 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[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.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "starter" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.1.3" }, + { name = "pytest", specifier = ">=9.1.1" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/34/4dd12fc8bb7d61c91467ec3efe415ffa7d5456f799954b40c5bbaeae470e/werkzeug-3.1.9.tar.gz", hash = "sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060", size = 940188, upload-time = "2026-09-27T18:33:41.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/38/df03f564f43cec2684823f3cccae1a652ee7face1cbaa76fb223096e64d7/werkzeug-3.1.9-py3-none-any.whl", hash = "sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab", size = 228700, upload-time = "2026-09-27T18:33:39.685Z" }, +] diff --git a/Semaine_15/Jour_04/atelier-web/starter/verif_poste.py b/Semaine_15/Jour_04/atelier-web/starter/verif_poste.py new file mode 100644 index 0000000..cdd064d --- /dev/null +++ b/Semaine_15/Jour_04/atelier-web/starter/verif_poste.py @@ -0,0 +1,79 @@ +"""Vérification du poste avant l'atelier. + +Lancez simplement : python verif_poste.py +Puis copiez-collez TOUT le résultat au formateur. + +Ce script ne modifie rien et n'installe rien. +""" + +import platform +import sys + +MINIMUM = (3, 9) + + +def version_python(): + v = sys.version_info + actuelle = f"{v.major}.{v.minor}.{v.micro}" + if (v.major, v.minor) >= MINIMUM: + return True, f"Python {actuelle}" + return False, (f"Python {actuelle} — TROP ANCIEN, il faut au moins " + f"{MINIMUM[0]}.{MINIMUM[1]}") + + +def module_present(lastname): + try: + __import__(lastname) + except ImportError: + return False, f"{lastname} : ABSENT" + try: + from importlib.metadata import version + return True, f"{lastname} : {version(lastname)}" + except Exception: + return True, f"{lastname} : présent" + + +def dans_un_venv(): + actif = sys.prefix != getattr(sys, "base_prefix", sys.prefix) + if actif: + return True, "environnement virtuel : actif" + return True, "environnement virtuel : aucun (ce n'est pas bloquant)" + + +def main(): + print("=" * 56) + print(" VERIFICATION DU POSTE - ATELIER PYTHON WEB") + print("=" * 56) + print(f" Systeme : {platform.system()} {platform.release()}") + print(f" Executable : {sys.executable}") + print("-" * 56) + + resultats = [version_python(), dans_un_venv()] + for lastname in ("flask", "pytest"): + resultats.append(module_present(lastname)) + + for ok, message in resultats: + print((" [ OK ] " if ok else " [ KO ] ") + message) + + print("-" * 56) + + bloquant = not resultats[0][0] + modules = zip(("flask", "pytest"), resultats[2:], strict=False) + manquants = [lastname for lastname, (ok, _) in modules if not ok] + + if bloquant: + print(" ACTION : installez une version recente de Python depuis") + print(" python.org, en cochant 'Add python.exe to PATH'.") + print(" Prevenez le formateur des ce soir.") + elif manquants: + print(" ACTION : lancez cette commande, puis relancez ce script :") + print() + print(" python -m pip install " + " ".join(manquants)) + else: + print(" Tout est pret. Rien a faire.") + + print("=" * 56) + + +if __name__ == "__main__": + main()