Skip to content

Latest commit

 

History

History
1313 lines (1050 loc) · 36.2 KB

File metadata and controls

1313 lines (1050 loc) · 36.2 KB

📚 Documentation Complète — Projet Med-Info

Documentation autonome et intemporelle Conçue pour refaire le projet entièrement sans ressource externe


TABLE DES MATIÈRES

  1. Présentation du projet
  2. Architecture générale
  3. Prérequis et installation
  4. Version console (base du projet)
  5. Design Figma
  6. Version Flask — Structure
  7. Les templates HTML (Jinja2)
  8. Le CSS (style.css)
  9. Le Logging
  10. La base de données SQLite
  11. Concepts Python expliqués
  12. Concepts Flask expliqués
  13. Erreurs fréquentes et solutions
  14. Structure finale complète du projet

1. PRÉSENTATION DU PROJET

Qu'est-ce que Med-Info ?

Med-Info est une application web qui permet à n'importe quel utilisateur de rechercher des informations sur un médicament (usage, dosage, effets indésirables, avertissements) en français.

Pourquoi ce projet ?

  • Cible : grand public en Afrique francophone
  • Les informations médicales sur internet sont souvent en anglais
  • Med-Info interroge des APIs américaines (FDA + RxNorm) et traduit automatiquement en français

Stack technique

Langage    → Python 3
Framework  → Flask (web)
APIs       → FDA Drug Label + RxNorm (normalisation)
Traduction → deep-translator (Google Translate)
Base de données → SQLite (intégré Python)
Frontend   → HTML + CSS (Jinja2 templates)
Logging    → module logging (intégré Python)

2. ARCHITECTURE GÉNÉRALE

APPLI DE MÉDICAMENT/
│
├── api/
│   ├── fda_client.py        → appelle l'API FDA
│   ├── rxnorm_client.py     → normalise le nom du médicament
│   └── translator.py        → traduit EN → FR
│
├── models/
│   └── medicament.py        → classe Medicament (objet)
│
├── services/
│   └── recherche.py         → logique métier + cache DB
│
├── database/
│   └── db.py                → fonctions SQLite (CRUD)
│
├── templates/
│   ├── base.html            → template parent (navbar + footer)
│   ├── index.html           → page accueil
│   ├── resultat.html        → page résultat médicament
│   ├── historique.html      → page historique des recherches
│   └── a_propos.html        → page à propos
│
├── static/
│   └── css/
│       └── style.css        → styles (palette Figma)
│
├── utils/
│   └── formatage.py         → fonctions d'affichage (console)
│
├── app.py                   → point d'entrée Flask
├── main.py                  → point d'entrée console
├── med_info.db              → base de données SQLite (auto-créé)
├── med_info.log             → fichier de logs (auto-créé)
└── .gitignore               → fichiers à exclure de Git

Principe SRP (Single Responsibility Principle)

Chaque fichier a UNE seule responsabilité :

  • fda_client.py → SEULEMENT appeler l'API FDA
  • translator.py → SEULEMENT traduire
  • db.py → SEULEMENT gérer la base de données
  • app.py → SEULEMENT définir les routes Flask

C'est une bonne pratique professionnelle : si quelque chose casse, on sait exactement où chercher.


3. PRÉREQUIS ET INSTALLATION

Créer l'environnement virtuel

# Windows
python -m venv venv
venv\Scripts\activate

# Mac/Linux
python -m venv venv
source venv/bin/activate

Quand l'environnement est actif, tu vois (venv) au début du terminal.

Pourquoi un environnement virtuel ? Sans venv, toutes les bibliothèques s'installent globalement sur ton ordinateur. Avec venv, chaque projet a ses propres bibliothèques isolées → pas de conflits entre projets.

Installer les dépendances

pip install flask requests deep-translator

Ce que chaque bibliothèque fait :

  • flask → framework web pour créer les routes et les templates
  • requests → faire des appels HTTP vers les APIs externes
  • deep-translator → traduire du texte EN → FR via Google Translate

Note : sqlite3 et logging sont déjà intégrés à Python, pas besoin de les installer.

Créer le fichier .gitignore

Avant tout push Git, crée ce fichier à la racine :

venv/
__pycache__/
*.pyc
med_info.log
med_info.db
.env

Explication de chaque ligne :

  • venv/ → 200+ fichiers de bibliothèques, inutile de les versionner
  • __pycache__/ → fichiers compilés auto par Python
  • *.pyc → bytecode Python compilé
  • med_info.log → données privées des utilisateurs
  • med_info.db → base de données (peut contenir des données sensibles)
  • .env → futurs secrets (clés API, mots de passe)

4. VERSION CONSOLE — BASE DU PROJET

models/medicament.py

class Medicament:
    def __init__(self, name, usage, dosage, adverse_reactions, warnings):
        self.name = name
        self.usage = usage
        self.dosage = dosage
        self.adverse_reactions = adverse_reactions
        self.warnings = warnings

Cette classe représente un médicament comme un objet Python. Les "dunder methods" (str, repr, eq) permettent d'afficher et comparer les objets proprement.

api/fda_client.py

Rôle : Interroger l'API FDA américaine pour récupérer les infos d'un médicament.

URL de l'API :

https://api.fda.gov/drug/label.json?search=openfda.generic_name:NOMMED&limit=1

Ce que l'API retourne (JSON) :

  • indications_and_usage → usage du médicament
  • dosage_and_administration → posologie
  • adverse_reactions → effets indésirables
  • warnings → avertissements
  • openfda.brand_name → nom de marque

api/rxnorm_client.py

Rôle : Normaliser le nom du médicament avant d'interroger la FDA. Exemple : "doliprane" → "paracetamol" (nom générique standard)

Pourquoi c'est nécessaire ? L'API FDA ne reconnaît que les noms génériques anglais standardisés. RxNorm fait la conversion.

api/translator.py

from deep_translator import GoogleTranslator

def text_translator(sentence: str) -> str:
    if sentence == "" or sentence == "Non disponible":
        return "Non disponible"
    try:
        sentence = sentence[:4500]  # limite de l'API
        resultat = GoogleTranslator(source='en', target='fr').translate(sentence)
        return resultat
    except Exception:
        return sentence  # retourne l'original si erreur

Pourquoi [:4500] ? Google Translate a une limite de caractères par requête. On tronque à 4500 pour éviter les erreurs.

services/recherche.py (version originale avec cache dictionnaire)

cache = {}  # dictionnaire en mémoire (perdu à chaque redémarrage)

def search_medicament(medicament_name):
    if medicament_name in cache:
        return cache[medicament_name]  # retour immédiat si déjà cherché
    try:
        normalized_name = rxnorm_client.get_normalized_name(medicament_name)
        good_medicament_info = fda_client.get_medicament_data(normalized_name)
        if good_medicament_info is None:
            raise ValueError
        # Traduction de chaque champ
        ...
        cache[medicament_name] = medicament_fr  # sauvegarde en mémoire
        return medicament_fr
    except ValueError:
        return None

5. DESIGN FIGMA

Paramètres du frame

  • Device : iPhone 13 & 14
  • Dimensions : 390 × 844 px (mobile first)

Palette de couleurs

--fond:   #ebfcfb  → blanc menthe (fond principal)
--cartes: #cedfd9  → vert menthe doux (cartes médicaments)
--accent: #9b6a6c  → rose vieilli (accents)
--texte:  #5f5449  → brun foncé (textes)
--bleu:   #2D6BE4  → bleu médical (boutons, logo, highlights)
--blanc:  #ffffff

Règle 60-30-10

  • 60% → couleur de fond (#ebfcfb)
  • 30% → couleur secondaire (#cedfd9)
  • 10% → couleur d'accent (#2D6BE4)

Auto Layout dans Figma

Auto Layout = l'équivalent de Flexbox en CSS.

Pourquoi l'utiliser ?

  • Sans Auto Layout : les éléments sont positionnés à la main, tout se désaligne si on change quelque chose
  • Avec Auto Layout : les éléments s'organisent automatiquement et gardent leur espacement

Comment l'activer :

  1. Sélectionne un Frame (pas un Rectangle !)
  2. Shift + A → Auto Layout activé
  3. Régle la direction (horizontal ou vertical)
  4. Configure le spacing et le padding

Différence Frame vs Rectangle dans Figma :

  • Rectangle → forme simple, pas de padding possible
  • Frame → conteneur, supporte Auto Layout, padding, enfants

Convertir Rectangle en Frame : Clic droit → "Frame selection"

Structure des composants

iPhone 13 & 14 - 1 (frame principal)
│
├── Frame 2 (navbar)
│   ├── navbar (Auto Layout horizontal, H:56px, #ebfcfb)
│   │   ├── navbar hamburger (☰)
│   │   ├── Med-Info (texte, #2D6BE4, bold)
│   │   └── navbar logo (icône 💊)
│
├── Hero (Auto Layout vertical, padding 32px/20px, spacing 24px)
│   ├── Hero titre
│   ├── search bar
│   └── search button
│
├── section cartes (grid 2 colonnes)
│   ├── Card médicament 1
│   ├── Card médicament 2
│   └── ...
│
└── Footer (H:80px, Y:764px, #2D6BE4)
    ├── © 2026 Med-Info
    └── À propos | +228 97-17-73-73

6. VERSION FLASK — STRUCTURE

Qu'est-ce que Flask ?

Flask est un micro-framework web Python. Il permet de :

  • Définir des URLs (routes)
  • Recevoir des données de formulaires
  • Retourner des pages HTML dynamiques

app.py — Point d'entrée

from flask import Flask, render_template, request
from services.recherche import search_medicament
from database.db import init_db, ajouter_historique, get_historique
import urllib.parse
import logging

# Configuration logging (voir section 9)
logging.basicConfig(...)

app = Flask(__name__)
init_db()  # crée les tables SQLite au démarrage

# ROUTE 1 : Page accueil
@app.route("/")
def index():
    return render_template("index.html")

# ROUTE 2 : Résultat via formulaire (POST)
@app.route("/resultat", methods=["POST"])
def resultat():
    med_name = request.form.get("medicament", "").strip()
    if not med_name:
        return render_template("index.html", erreur="Veuillez entrer un nom.")
    medicament = search_medicament(med_name)
    if medicament is None:
        return render_template("index.html", erreur=f"'{med_name}' non trouvé.")
    ajouter_historique(med_name)
    return render_template("resultat.html", medicament=medicament)

# ROUTE 3 : Recherche directe via URL (cartes cliquables)
@app.route("/recherche/<path:nom>")
def recherche_directe(nom):
    nom = urllib.parse.unquote(nom).strip()
    medicament = search_medicament(nom)
    if medicament is None:
        return render_template("index.html", erreur=f"'{nom}' non trouvé.")
    ajouter_historique(nom)
    return render_template("resultat.html", medicament=medicament)

# ROUTE 4 : Historique
@app.route("/historique")
def historique():
    recherches = get_historique(limite=20)
    return render_template("historique.html", recherches=recherches)

# ROUTE 5 : À propos
@app.route("/a-propos")
def a_propos():
    return render_template("a_propos.html")

if __name__ == "__main__":
    app.run(debug=True)

Explication des routes

GET vs POST :

  • GET → récupère une page (URL tapée dans le navigateur, lien cliqué)
  • POST → envoie des données (formulaire soumis)

<path:nom> vs <nom> :

  • <nom> : accepte les textes simples, bloque les "/" et les espaces
  • <path:nom> : accepte tout, y compris les "/" et les espaces encodés

urllib.parse.unquote(nom) : Les URLs ne peuvent pas contenir d'espaces. L'espace est encodé en %20. unquote() convertit %20 → espace lisible par l'API. Exemple : ascorbic%20acidascorbic acid

.strip() : Supprime les espaces invisibles au début et à la fin d'une chaîne. C'est une pratique défensive pour nettoyer les données venant de l'extérieur.


7. LES TEMPLATES HTML (JINJA2)

Qu'est-ce que Jinja2 ?

Jinja2 est le moteur de templates intégré à Flask. Il permet d'insérer du Python dans du HTML.

Syntaxe de base :

{{ variable }}              → affiche une variable
{% if condition %}          → condition
{% for item in liste %}     → boucle
{% extends "base.html" %}   → hérite d'un template parent
{% block content %}         → définit une zone remplaçable

base.html — Template parent

Toutes les autres pages "héritent" de base.html. base.html contient les éléments communs à toutes les pages :

  • La structure HTML (DOCTYPE, head, meta, link CSS)
  • La navbar
  • Le footer
  • {% block content %} → zone remplacée par chaque page enfant
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Med-Info</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav class="navbar">
        <span class="hamburger"></span>
        <span class="logo">Med-Info</span>
        <span class="nav-icon">💊</span>
    </nav>

    <main>
        <div class="container">
            {% block content %}{% endblock %}
        </div>
    </main>

    <footer class="footer">
        <p>© 2026 Med-Info</p>
        <p>
            <a href="/a-propos">À propos</a> |
            <a href="/historique">Historique</a> |
            +228 97-17-73-73
        </p>
    </footer>
</body>
</html>

Pourquoi url_for('static', filename='css/style.css') ? Au lieu d'écrire le chemin en dur, Flask génère l'URL correcte automatiquement. C'est plus robuste lors du déploiement.

index.html — Page accueil

{% extends "base.html" %}
{% block content %}

<section class="hero">
    <div class="hero-emoji">💊</div>
    <h1>Trouvez les infos <span class="accent">Clés</span> sur vos médicaments</h1>

    {% if erreur %}
        <p class="erreur">{{ erreur }}</p>
    {% endif %}

    <form action="/resultat" method="POST">
        <div class="search-bar">
            <span>🔍</span>
            <input type="text" name="medicament" placeholder="Rechercher...">
        </div>
        <button type="submit" class="btn-rechercher">Rechercher</button>
    </form>
</section>

<h2 class="titre-cartes">💊 Médicaments fréquents</h2>

<section class="section-cartes">
    <a href="/recherche/paracetamol" class="card">
        <strong>Paracétamol</strong><span>Antidouleur</span>
    </a>
    <!-- ... 19 autres cartes ... -->
</section>

{% endblock %}

Points importants :

  • method="POST" → les données du formulaire sont envoyées en POST
  • name="medicament" → c'est ce nom qu'on récupère avec request.form.get("medicament")
  • {% if erreur %} → affiche le message d'erreur si Flask en envoie un
  • Les cartes <a href="/recherche/nom"> → cliquables, lancent une recherche directe

resultat.html — Page résultat

{% extends "base.html" %}
{% block content %}

<section class="hero">
    <h1>💊 {{ medicament.name }}</h1>

    <div class="card-resultat">
        <h3>Usage</h3>
        <p>{{ medicament.usage }}</p>
    </div>
    <div class="card-resultat">
        <h3>Dosage</h3>
        <p>{{ medicament.dosage }}</p>
    </div>
    <div class="card-resultat">
        <h3>Effets indésirables</h3>
        <p>{{ medicament.adverse_reactions }}</p>
    </div>
    <div class="card-resultat">
        <h3>Avertissements</h3>
        <p>{{ medicament.warnings }}</p>
    </div>

    <a href="/" class="btn-rechercher" style="text-align:center; text-decoration:none; display:block;">
        ← Nouvelle recherche
    </a>
</section>

{% endblock %}

Comment Flask passe les données au template ?

# Dans app.py
return render_template("resultat.html", medicament=medicament)
# "medicament" est maintenant accessible dans le template via {{ medicament.name }}

historique.html — Page historique

{% extends "base.html" %}
{% block content %}

<section class="hero">
    <h1>🕐 Historique des <span class="accent">recherches</span></h1>

    {% if recherches %}
        <section class="section-cartes">
            {% for r in recherches %}
            <a href="/recherche/{{ r['nom_recherche'] }}" class="card">
                <strong>{{ r['nom_recherche'] }}</strong>
                <span>{{ r['date_recherche'] }}</span>
            </a>
            {% endfor %}
        </section>
    {% else %}
        <p>Aucune recherche effectuée pour le moment.</p>
    {% endif %}

    <a href="/" class="btn-rechercher"
       style="text-align:center; text-decoration:none; display:block; margin-top:24px;">
        ← Retour à l'accueil
    </a>
</section>

{% endblock %}

8. LE CSS (style.css)

/* VARIABLES (palette Figma) */
:root {
    --fond:   #ebfcfb;
    --cartes: #cedfd9;
    --accent: #9b6a6c;
    --texte:  #5f5449;
    --bleu:   #2D6BE4;
    --blanc:  #ffffff;
}

/* RESET */
* { margin: 0; padding: 0; box-sizing: border-box; }

/* FORCE MODE CLAIR (évite le dark mode du navigateur) */
html { color-scheme: light; }

/* BODY — STICKY FOOTER */
body {
    background-color: var(--fond);
    color: var(--texte);
    font-family: sans-serif;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    min-height: 100vh;  /* au moins toute la hauteur de l'écran */
}

/* MAIN prend tout l'espace disponible → footer poussé en bas */
main { flex: 1; }

/* RESPONSIVE */
.container { max-width: 390px; margin: 0 auto; padding: 0 16px; }

@media (min-width: 768px) {
    .container { max-width: 600px; }
    .hero h1 { font-size: 32px; }
}

@media (min-width: 1024px) {
    .container { max-width: 800px; }
}

/* NAVBAR */
.navbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16px 20px;
    background-color: var(--fond);
    border-bottom: 1px solid var(--cartes);
    height: 56px;
}
.logo { color: var(--bleu); font-weight: bold; font-size: 18px; }

/* HERO */
.hero {
    padding: 32px 20px;
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.hero-emoji { font-size: 40px; text-align: center; }
.hero h1 { font-size: 24px; font-weight: normal; }
.hero h1 .accent { color: var(--bleu); font-weight: bold; }

/* SEARCH BAR */
.search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    border: 1px solid var(--texte);
    border-radius: 8px;
    padding: 12px 16px;
    background: var(--blanc);
}
.search-bar input {
    border: none;
    outline: none;
    width: 100%;
    font-size: 16px;
}

/* BOUTON */
.btn-rechercher {
    width: 100%;
    padding: 16px;
    background-color: var(--bleu);
    color: var(--blanc);
    border: none;
    border-radius: 50px;
    font-size: 16px;
    font-weight: bold;
    cursor: pointer;
}

/* CARTES — GRILLE 2 COLONNES */
.section-cartes {
    padding: 0 20px 32px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

.card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    background: var(--cartes);
    padding: 16px;
    border-radius: 12px;
    height: 80px;
    text-decoration: none;
    color: var(--texte);
    cursor: pointer;
    transition: background 0.2s ease;
    gap: 4px;
}
.card:hover { background: #b8cec8; }

/* CARTE RÉSULTAT */
.card-resultat {
    background: var(--cartes);
    border-radius: 12px;
    padding: 16px;
}
.card-resultat h3 {
    color: var(--bleu);
    margin-bottom: 8px;
    font-size: 14px;
    text-transform: uppercase;
}
.card-resultat p { font-size: 14px; line-height: 1.6; }

/* TITRE SECTION CARTES */
.titre-cartes {
    padding: 0 20px 12px;
    font-size: 16px;
    color: var(--texte);
    font-weight: bold;
}

/* ERREUR */
.erreur { color: red; font-size: 14px; }

/* FOOTER */
.footer {
    background-color: var(--bleu);
    color: var(--blanc);
    text-align: center;
    padding: 12px 16px;
    font-size: 14px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.footer a { color: var(--blanc); text-decoration: underline; }
.footer a:hover { color: var(--cartes); }

Explication du Sticky Footer : Le problème : sur les pages avec peu de contenu, le footer remontait au milieu de la page. La solution :

body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; }

min-height: 100vh → le body fait au moins 100% de la hauteur de l'écran flex: 1 sur main → main s'étire pour remplir l'espace disponible → le footer est toujours poussé tout en bas


9. LE LOGGING

Pourquoi utiliser logging et pas print() ?

print()    → affiche dans le terminal, pas de date, pas de niveau,
             pas sauvegardable, pas filtrable

logging    → date + heure automatique, niveaux de gravité,
             sauvegardable dans fichier, filtrable

Les 5 niveaux (du moins grave au plus grave)

logging.debug("Détail technique")     # niveau 10
logging.info("Info normale")          # niveau 20
logging.warning("Attention !")        # niveau 30
logging.error("Erreur !")             # niveau 40
logging.critical("Erreur fatale !")   # niveau 50

Configuration utilisée dans Med-Info

import logging

logging.basicConfig(
    level=logging.INFO,                          # affiche INFO et au-dessus
    format="%(asctime)s %(levelname)-8s %(message)s",  # format du message
    datefmt="%Y-%m-%d %H:%M:%S",                # format de la date
    handlers=[
        logging.FileHandler("med_info.log"),     # sauvegarde dans fichier
        logging.StreamHandler()                  # affiche aussi dans terminal
    ]
)

Explication du format :

  • %(asctime)s → date et heure (définie par datefmt)
  • %(levelname)-8s → niveau sur 8 caractères (INFO, WARNING...)
  • %(message)s → le message du log

Résultat dans le terminal/fichier :

2026-04-21 14:32:01 INFO     Recherche lancée : paracetamol
2026-04-21 14:32:03 INFO     Médicament trouvé : TYLENOL
2026-04-21 14:33:10 WARNING  Médicament non trouvé : xyzabc

Différence INFO majuscule vs info minuscule :

logging.INFOconstante = nombre (20) → utilisé dans basicConfig(level=...)
logging.info()  → fonction = actionutilisé pour écrire un message

Règle d'or : basicConfig() doit TOUJOURS être appelé en premier, avant tout autre log.

Modes du FileHandler :

logging.FileHandler("med_info.log")          # mode "a" par défaut (append)
logging.FileHandler("med_info.log", mode="w") # mode "w" vide le fichier au démarrage

Logs dans Med-Info :

# Recherche vide
logging.warning("Recherche vide soumise")

# Recherche lancée
logging.info(f"Recherche lancée : {med_name}")

# Médicament trouvé
logging.info(f"Médicament trouvé : {medicament.name}")

# Médicament non trouvé
logging.warning(f"Médicament non trouvé : {med_name}")

# Cache DB hit (pas d'appel API nécessaire)
logging.info(f"Cache DB hit : {medicament_name}")

10. LA BASE DE DONNÉES SQLITE

Qu'est-ce que SQLite ?

SQLite est une base de données stockée dans un seul fichier sur ton ordinateur. Contrairement à MySQL ou PostgreSQL, il n'y a pas de serveur à installer. Le fichier med_info.db contient toute la base de données.

SQLite vs SQLAlchemy

SQLite direct  → tu écris le SQL toi-même
                 conn.execute("SELECT * FROM table")
                 adapté pour apprendre, projets simples

SQLAlchemy     → bibliothèque qui génère le SQL automatiquement
                 db.session.query(Medicament).all()
                 adapté pour projets Flask moyens/grands

Les deux utilisent SQLite comme moteur de stockage. La différence c'est la façon d'y accéder.

Les opérations CRUD

CREATE → créer une table
INSERT → ajouter une ligne
SELECT → lire des données
UPDATE → modifier une ligne
DELETE → supprimer une ligne

database/db.py — Code complet expliqué

import sqlite3
import logging
from datetime import datetime

DB_PATH = "med_info.db"  # nom du fichier de base de données


def get_connection():
    """Ouvre une connexion au fichier .db"""
    conn = sqlite3.connect(DB_PATH)
    # Si le fichier n'existe pas, SQLite le crée automatiquement

    conn.row_factory = sqlite3.Row
    # Sans row_factory : resultat[0], resultat[1]... (accès par index)
    # Avec row_factory : resultat["nom_recherche"] (accès par nom de colonne)

    return conn


def init_db():
    """Crée les tables au premier lancement"""
    conn = get_connection()
    cursor = conn.cursor()  # le curseur = stylo qui écrit dans la DB

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS historique (
            id             INTEGER PRIMARY KEY AUTOINCREMENT,
            nom_recherche  TEXT NOT NULL,
            date_recherche TEXT NOT NULL
        )
    """)
    # INTEGER PRIMARY KEY AUTOINCREMENT → id généré auto (1, 2, 3...)
    # TEXT NOT NULL → champ obligatoire, ne peut pas être vide
    # IF NOT EXISTS → pas d'erreur si la table existe déjà

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS medicaments_cache (
            id                INTEGER PRIMARY KEY AUTOINCREMENT,
            nom_recherche     TEXT NOT NULL UNIQUE,
            nom               TEXT,
            usage             TEXT,
            dosage            TEXT,
            adverse_reactions TEXT,
            warnings          TEXT,
            date_ajout        TEXT NOT NULL
        )
    """)
    # UNIQUE → pas de doublons sur nom_recherche
    # Nécessaire pour que INSERT OR REPLACE fonctionne

    conn.commit()  # valide et sauvegarde les changements
    conn.close()   # ferme la connexion (libère la mémoire)
    logging.info("Base de données initialisée ✅")


def ajouter_historique(nom_recherche: str):
    """Enregistre une recherche réussie dans l'historique"""
    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute(
        "INSERT INTO historique (nom_recherche, date_recherche) VALUES (?, ?)",
        (nom_recherche, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    )
    # Les "?" sont des placeholders sécurisés
    # JAMAIS concatener directement : "VALUES ('" + nom + "')"
    # → vulnérable aux injections SQL

    conn.commit()
    conn.close()
    logging.info(f"Historique ajouté : {nom_recherche}")


def get_historique(limite: int = 10):
    """Retourne les dernières recherches"""
    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute(
        """SELECT nom_recherche, date_recherche
           FROM historique
           ORDER BY id DESC
           LIMIT ?""",
        (limite,)
    )
    # ORDER BY id DESC → du plus récent au plus ancien
    # LIMIT ? → nombre maximum de résultats

    resultats = cursor.fetchall()  # récupère TOUTES les lignes
    conn.close()
    return resultats


def get_cache_medicament(nom_recherche: str):
    """Cherche un médicament dans le cache"""
    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute(
        "SELECT * FROM medicaments_cache WHERE nom_recherche = ?",
        (nom_recherche,)
    )

    resultat = cursor.fetchone()  # récupère UNE seule ligne (ou None)
    conn.close()
    return resultat  # None si pas trouvé


def sauvegarder_cache_medicament(nom_recherche: str, medicament):
    """Sauvegarde un médicament dans le cache"""
    conn = get_connection()
    cursor = conn.cursor()

    cursor.execute("""
        INSERT OR REPLACE INTO medicaments_cache
        (nom_recherche, nom, usage, dosage, adverse_reactions, warnings, date_ajout)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """,
    (
        nom_recherche,
        medicament.name,
        medicament.usage,
        medicament.dosage,
        medicament.adverse_reactions,
        medicament.warnings,
        datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    ))
    # INSERT OR REPLACE → si le médicament existe déjà (UNIQUE), le remplace
    # Évite les erreurs de doublons

    conn.commit()
    conn.close()
    logging.info(f"Cache DB sauvegardé : {nom_recherche}")

services/recherche.py — Version avec cache DB

from api import fda_client, rxnorm_client, translator
from models.medicament import Medicament
from database.db import get_cache_medicament, sauvegarder_cache_medicament
import logging

def search_medicament(medicament_name: str):
    """
    Logique de recherche en 3 étapes :
    1. Vérifier le cache DB (ultra rapide)
    2. Si absent → appeler les APIs (lent : 3-5 secondes)
    3. Sauvegarder dans le cache DB (pour les prochaines fois)
    """

    # ÉTAPE 1 : Cache DB
    cache = get_cache_medicament(medicament_name)
    if cache:
        logging.info(f"Cache DB hit : {medicament_name}")
        return Medicament(
            cache["nom"],
            cache["usage"],
            cache["dosage"],
            cache["adverse_reactions"],
            cache["warnings"]
        )

    # ÉTAPE 2 : APIs
    try:
        normalized_name = rxnorm_client.get_normalized_name(medicament_name)
        good_medicament_info = fda_client.get_medicament_data(normalized_name)
        if good_medicament_info is None:
            raise ValueError

        name_fr              = translator.text_translator(good_medicament_info.name)
        usage_fr             = translator.text_translator(good_medicament_info.usage)
        dosage_fr            = translator.text_translator(good_medicament_info.dosage)
        adverse_reactions_fr = translator.text_translator(good_medicament_info.adverse_reactions)
        warnings_fr          = translator.text_translator(good_medicament_info.warnings)

        medicament_fr = Medicament(name_fr, usage_fr, dosage_fr, adverse_reactions_fr, warnings_fr)

        # ÉTAPE 3 : Sauvegarder
        sauvegarder_cache_medicament(medicament_name, medicament_fr)

        return medicament_fr

    except ValueError:
        return None

Historique vs Logs — Quelle différence ?

LOGS                          HISTORIQUE DB
─────────────────────────────────────────────────────
Pour le DÉVELOPPEUR           Pour l'UTILISATEUR
Fichier texte brut            Base de données structurée
Tout type d'événements        Seulement les recherches réussies
Erreurs incluses              Pas d'erreurs
Difficile à filtrer/trier     Facile à filtrer/trier SQL
Non consultable dans l'app    Consultable dans l'app (/historique)

11. CONCEPTS PYTHON EXPLIQUÉS

urllib.parse

Module intégré Python pour manipuler les URLs.

import urllib.parse

# unquote : URL → texte lisible
urllib.parse.unquote("ascorbic%20acid")  # → "ascorbic acid"

# quote : texte → URL
urllib.parse.quote("ascorbic acid")      # → "ascorbic%20acid"

# urlencode : dict → paramètres URL
params = {"search": "paracetamol", "limit": 1}
urllib.parse.urlencode(params)           # → "search=paracetamol&limit=1"

Convention de nommage Python (PEP8)

variables_et_fonctions = "snake_case"   # tout en minuscules, _ entre mots
ClassesEtModeles       = "PascalCase"   # majuscule à chaque mot
CONSTANTES             = "MAJUSCULES"   # tout en majuscules

.strip()

nom = "  paracetamol  "
nom.strip()   # → "paracetamol"  (supprime espaces au début et à la fin)

Utilisé pour nettoyer les données venant de l'extérieur (formulaires, URLs).

Type hints

def get_cache_medicament(nom_recherche: str):        # paramètre de type str
def get_historique(limite: int = 10):                # int avec valeur par défaut
def search_medicament(medicament_name: str) -> None: # retourne None

Les type hints n'obligent pas Python à vérifier les types (Python reste dynamique), mais ils documentent le code et aident VS Code à faire l'autocomplétion.


12. CONCEPTS FLASK EXPLIQUÉS

render_template()

# Retourne un fichier HTML (avec variables injectées)
return render_template("index.html")
return render_template("resultat.html", medicament=medicament)
# "medicament" est maintenant accessible dans le template

request.form.get()

# Récupère la valeur d'un champ de formulaire envoyé en POST
med_name = request.form.get("medicament", "").strip()
# "medicament" → correspond au name= dans le HTML
# "" → valeur par défaut si le champ est absent

url_for()

# Dans le HTML : génère l'URL correcte vers un fichier statique
{{ url_for('static', filename='css/style.css') }}
# → /static/css/style.css

Héritage de templates Jinja2

<!-- base.html -->
{% block content %}{% endblock %}

<!-- index.html -->
{% extends "base.html" %}
{% block content %}
    <!-- contenu spécifique à cette page -->
{% endblock %}

13. ERREURS FRÉQUENTES ET SOLUTIONS

Erreur : "source n'est pas reconnu" (Windows)

Cause   : commande Mac/Linux tapée sur Windows
Solution: utiliser venv\Scripts\activate (Windows)

Erreur : Auto Layout non disponible dans Figma

Cause   : l'élément sélectionné est un Rectangle, pas un Frame
Solution: clic droit → "Frame selection", puis Shift+A

Erreur : les couleurs semblent sombres dans le navigateur

Cause   : dark mode du navigateur
Solution: html { color-scheme: light; }

Erreur : footer ne colle pas en bas

Cause   : body n'est pas en flexbox
Solution:
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; }

Erreur : médicament non trouvé malgré nom correct

Causes possibles :
1. Espace dans l'URL → utiliser urllib.parse.unquote()
2. Faute dans le nom de table SQLite → vérifier la cohérence
3. UNIQUE manquant → INSERT OR REPLACE ne fonctionne pas

Erreur : SyntaxError dans basicConfig

Cause   : virgule manquante entre les paramètres
Solution: vérifier que chaque paramètre se termine par une virgule
          sauf le dernier

14. STRUCTURE FINALE COMPLÈTE

Tous les fichiers

APPLI DE MÉDICAMENT/
│
├── api/
│   ├── __init__.py
│   ├── fda_client.py
│   ├── rxnorm_client.py
│   └── translator.py
│
├── models/
│   ├── __init__.py
│   └── medicament.py
│
├── services/
│   ├── __init__.py
│   └── recherche.py        ← modifié : cache DB
│
├── database/
│   └── db.py               ← nouveau
│
├── utils/
│   └── formatage.py
│
├── templates/
│   ├── base.html           ← modifié : container + liens footer
│   ├── index.html          ← modifié : 20 cartes + grille
│   ├── resultat.html
│   ├── historique.html     ← nouveau
│   └── a_propos.html
│
├── static/
│   └── css/
│       └── style.css       ← modifié : grid + sticky footer
│
├── app.py                  ← modifié : init_db + historique + routes
├── main.py                 ← inchangé (version console)
├── med_info.db             ← auto-créé au premier lancement
├── med_info.log            ← auto-créé au premier lancement
└── .gitignore

Commandes pour lancer le projet

# 1. Activer l'environnement virtuel
venv\Scripts\activate

# 2. Lancer l'application
python app.py

# 3. Ouvrir dans le navigateur
http://127.0.0.1:5000

Pages disponibles

http://127.0.0.1:5000/              → Accueil
http://127.0.0.1:5000/resultat      → Résultat (POST)
http://127.0.0.1:5000/recherche/nom → Recherche directe
http://127.0.0.1:5000/historique    → Historique
http://127.0.0.1:5000/a-propos      → À propos

✅ Checklist — Refaire le projet from scratch

  • Créer le dossier du projet
  • python -m venv venv + activation
  • pip install flask requests deep-translator
  • Créer .gitignore
  • Créer models/medicament.py
  • Créer api/fda_client.py
  • Créer api/rxnorm_client.py
  • Créer api/translator.py
  • Créer services/recherche.py (avec cache DB)
  • Créer database/db.py
  • Créer app.py
  • Créer templates/base.html
  • Créer templates/index.html (avec 20 médicaments)
  • Créer templates/resultat.html
  • Créer templates/historique.html
  • Créer templates/a_propos.html
  • Créer static/css/style.css
  • python app.py → tester toutes les routes
  • Vérifier que med_info.db est créé automatiquement
  • Vérifier que med_info.log est créé automatiquement
  • git init + git add . + git commit
  • git push sur GitHub