Développement d'une API Web en Python : Gestion Complète d'une Liste de Tâches

Pour finalisre notre application de gestion de tâches, nous devons implémenter les opérations de création, de modification et de suppression. Chaque fonctionnalité nécessitera des ajustements au niveau des modèles, des contrôleurs et des vues.

Implémentasion de la création

Le processus d'ajout d'une tâche suit un flux spécifique : saisie de l'utilisateur, envoi via une requête POST, traitement côté serveur, persistance des données et redirection vers la page principale.

Commençons par le gabarit HTML. Un formulaire est nécessaire pour capturer l'entrée :

<!-- templates/accueil.html -->

<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Gestionnaire de Tâches</title>
  <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
  <div class="wrapper">
    <h1>Mes Tâches</h1>
    <form method="post" action="/ajouter" class="input-group">
      <input type="text" name="texte" placeholder="Nouvelle tâche..." required>
      <button type="submit" class="btn-add">Créer</button>
    </form>
    <ul class="task-list">
      {% for tache in liste_taches %}
      <li><span>{{ tache.texte }}</span></li>
      {% endfor %}
    </ul>
  </div>
</body>
</html>

Le style CSS associé :

/* static/styles.css */
.input-group { display: flex; gap: 10px; max-width: 500px; margin-bottom: 20px; }
.input-group input { flex-grow: 1; padding: 8px; }
.btn-add { padding: 8px 15px; cursor: pointer; }

Le serveur doit désormais interpréter les données envoyées dans le corps des requêtes POST. Nous étendons le parseur de requête :

# utils/http_parser.py
from urllib.parse import unquote_plus

class RequeteHTTP:
    def __init__(self, flux_brut):
        self.methode, self.chemin, self.en_tetes, self.params_url, self.donnees = self._analyser(flux_brut)

    def _analyser(self, flux):
        bloc_en_tete, bloc_corps = flux.split('\r\n\r\n', 1)
        ligne_req, *rest = bloc_en_tete.split('\r\n')
        methode, chemin_param, _ = ligne_req.split()
        chemin, params = self._extraire_params(chemin_param)
        dico_headers = {}
        for ligne in rest:
            cle, val = ligne.split(': ', 1)
            dico_headers[cle] = val
        return methode, chemin, dico_headers, params, self._decoder_corps(bloc_corps)

    @staticmethod
    def _extraire_params(data):
        if '?' not in data:
            return data, {}
        url, query = data.split('?', 1)
        dico = {p.split('=')[0]: p.split('=', 1)[1] for p in query.split('&') if '=' in p}
        return url, dico

    @staticmethod
    def _decoder_corps(data):
        if not data.strip():
            return {}
        return {p.split('=')[0]: unquote_plus(p.split('=', 1)[1]) for p in data.split('&') if '=' in p}

Une fois les données enregistrées, le navigateur doit être redirigé. Nous ajoutons une fonction utilitaire pour générer une réponse HTTP de redirection :

# utils/reponse.py
def redirection(vers_url, code_statut=302):
    phrases = {200: 'OK', 302: 'FOUND', 405: 'METHOD NOT ALLOWED'}
    entetes = {'Location': vers_url, 'Content-Length': '0'}
    ligne_statut = f'HTTP/1.1 {code_statut} {phrases.get(code_statut, "UNKNOWN")}'
    entetes_str = '\r\n'.join(f'{k}: {v}' for k, v in entetes.items())
    return f'{ligne_statut}\r\n{entetes_str}\r\n\r\n'

Dans la couche modèle, la classe Tache nécessite une méthode pour persister l'objet dans le fichier JSON :

# models/base.py
import json, os

class Tache:
    def __init__(self, identifiant=None, texte=""):
        self.identifiant = identifiant
        self.texte = texte

    @classmethod
    def _chemin_bd(cls):
        return os.path.join(os.path.dirname(__file__), 'db', 'donnees.json')

    @classmethod
    def charger(cls):
        with open(cls._chemin_bd(), 'r', encoding='utf-8') as f:
            return json.load(f)

    @classmethod
    def persister(cls, liste):
        with open(cls._chemin_bd(), 'w', encoding='utf-8') as f:
            json.dump(liste, f, indent=4, ensure_ascii=False)

    def enregistrer(self):
        liste_actuelle = [t.__dict__ for t in self._toutes() if t.identifiant != self.identifiant]
        if self.identifiant is None:
            dernier = liste_actuelle[-1]['identifiant'] if liste_actuelle else 0
            self.identifiant = dernier + 1
        liste_actuelle.append(self.__dict__)
        self.persister(liste_actuelle)

    @classmethod
    def _toutes(cls):
        return [cls(**d) for d in cls.charger()]

Le contrôleur gère désormais la route /ajouter :

# controllers/taches.py
from models.base import Tache
from utils.reponse import redirection

def creer_tache(requete):
    contenu = requete.donnees.get("texte", "").strip()
    if contenu:
        nouvelle = Tache(texte=contenu)
        nouvelle.enregistrer()
    return redirection("/")

chemins = {
    '/ajouter': (creer_tache, ['POST'])
}

Modification d'une entrée

L'édition nécessite deux étapes : afficher le formulaire pré-rempli (via GET) et traiter la soumission (POST). Bien que PUT soit sémantiquement plus précis pour le REST, nous conservons POST pour uniformiser le traitement dans ce framework minimaliste.

Gabararit d'édition :

<!-- templates/modifier.html -->

<html lang="fr">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
  <div class="wrapper">
    <h2>Modifier la tâche</h2>
    <form method="post" action="/modifier" class="input-group">
      <input type="hidden" name="id" value="{{ tache.identifiant }}">
      <input type="text" name="texte" value="{{ tache.texte }}">
      <button type="submit">Valider</button>
    </form>
  </div>
</body>
</html>

Nous devons également pouvoir interroger le modèle par critère. Ajoutons des méthodes de recherche :

# models/base.py (extension)
    @classmethod
    def filtrer(cls, **criteres):
        resultats = []
        for element in cls.charger():
            match = True
            for cle, val in criteres.items():
                if str(element.get(cle)) != str(val):
                    match = False
                    break
            if match:
                resultats.append(cls(**element))
        return resultats

    @classmethod
    def obtenir_par_id(cls, identifiant):
        trouve = cls.filtrer(identifiant=identifiant)
        return trouve[0] if trouve else None

Le contrôleur doit désormais gérer les deux verbes HTTP :

# controllers/taches.py (extension)
from utils.template import render_template # Fonction fictive de rendu

def gerer_modification(requete):
    if requete.methode == 'POST':
        id_cible = int(requete.donnees.get('id', -1))
        nouveau_texte = requete.donnees.get('texte', '').strip()
        if id_cible != -1 and nouveau_texte:
            cible = Tache.obtenir_par_id(id_cible)
            if cible:
                cible.texte = nouveau_texte
                cible.enregistrer()
                return redirection("/")
    else:
        id_cible = int(requete.params_url.get('id', -1))
        cible = Tache.obtenir_par_id(id_cible)
        if not cible:
            return redirection("/")
        return render_template("modifier.html", tache=cible)

chemins.update({
    '/modifier': (gerer_modification, ['GET', 'POST'])
})

Suppression d'une entrée

La logique de suppression suit le même schéma : un formulaire caché envoie l'identifiant, le contrôleur valide, le modèle retire l'entrée du fichier et le serveur redirige.

Ajout du bouton dans la liste :

{% for tache in liste_taches %}
<li>
  <span>{{ tache.texte }}</span>
  <div class="actions">
    <a href="/modifier?id={{ tache.identifiant }}"><button>Éditer</button></a>
    <form method="post" action="/supprimer" style="display:inline">
      <input type="hidden" name="id" value="{{ tache.identifiant }}">
      <button type="submit" class="btn-del">Supprimer</button>
    </form>
  </div>
</li>
{% endfor %}

Méthode de suppression dans le modèle :

# models/base.py (extension)
    def supprimer(self):
        reste = [t.__dict__ for t in self._toutes() if t.identifiant != self.identifiant]
        self.persister(reste)

Et le contrôleur final :

# controllers/taches.py (extension)
def traiter_suppression(requete):
    id_cible = int(requete.donnees.get('id', -1))
    cible = Tache.obtenir_par_id(id_cible)
    if cible:
        cible.supprimer()
    return redirection("/")

chemins.update({
    '/supprimer': (traiter_suppression, ['POST'])
})

Étiquettes: Python http-parser json-persistence custom-web-framework crud-architecture

Publié le 15 août à 08h06