Documentation autonome et intemporelle Conçue pour refaire le projet entièrement sans ressource externe
- Présentation du projet
- Architecture générale
- Prérequis et installation
- Version console (base du projet)
- Design Figma
- Version Flask — Structure
- Les templates HTML (Jinja2)
- Le CSS (style.css)
- Le Logging
- La base de données SQLite
- Concepts Python expliqués
- Concepts Flask expliqués
- Erreurs fréquentes et solutions
- Structure finale complète du projet
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.
- 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
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)
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
Chaque fichier a UNE seule responsabilité :
fda_client.py→ SEULEMENT appeler l'API FDAtranslator.py→ SEULEMENT traduiredb.py→ SEULEMENT gérer la base de donnéesapp.py→ SEULEMENT définir les routes Flask
C'est une bonne pratique professionnelle : si quelque chose casse, on sait exactement où chercher.
# Windows
python -m venv venv
venv\Scripts\activate
# Mac/Linux
python -m venv venv
source venv/bin/activateQuand 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.
pip install flask requests deep-translatorCe que chaque bibliothèque fait :
flask→ framework web pour créer les routes et les templatesrequests→ faire des appels HTTP vers les APIs externesdeep-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.
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 utilisateursmed_info.db→ base de données (peut contenir des données sensibles).env→ futurs secrets (clés API, mots de passe)
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 = warningsCette classe représente un médicament comme un objet Python. Les "dunder methods" (str, repr, eq) permettent d'afficher et comparer les objets proprement.
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édicamentdosage_and_administration→ posologieadverse_reactions→ effets indésirableswarnings→ avertissementsopenfda.brand_name→ nom de marque
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.
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 erreurPourquoi [:4500] ? Google Translate a une limite de caractères par requête. On tronque à 4500 pour éviter les erreurs.
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- Device : iPhone 13 & 14
- Dimensions : 390 × 844 px (mobile first)
--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
- 60% → couleur de fond (#ebfcfb)
- 30% → couleur secondaire (#cedfd9)
- 10% → couleur d'accent (#2D6BE4)
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 :
- Sélectionne un Frame (pas un Rectangle !)
- Shift + A → Auto Layout activé
- Régle la direction (horizontal ou vertical)
- 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"
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
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
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)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%20acid → ascorbic 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.
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çableToutes 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.
{% 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 POSTname="medicament"→ c'est ce nom qu'on récupère avecrequest.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
{% 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 }}{% 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 %}/* 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
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
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 50import 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.INFO → constante = nombre (20) → utilisé dans basicConfig(level=...)
logging.info() → fonction = action → utilisé pour écrire un messageRè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émarrageLogs 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}")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 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.
CREATE → créer une table
INSERT → ajouter une ligne
SELECT → lire des données
UPDATE → modifier une ligne
DELETE → supprimer une ligne
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}")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 NoneLOGS 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)
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"variables_et_fonctions = "snake_case" # tout en minuscules, _ entre mots
ClassesEtModeles = "PascalCase" # majuscule à chaque mot
CONSTANTES = "MAJUSCULES" # tout en majusculesnom = " 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).
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 NoneLes 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.
# 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# 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# Dans le HTML : génère l'URL correcte vers un fichier statique
{{ url_for('static', filename='css/style.css') }}
# → /static/css/style.css<!-- base.html -->
{% block content %}{% endblock %}
<!-- index.html -->
{% extends "base.html" %}
{% block content %}
<!-- contenu spécifique à cette page -->
{% endblock %}Cause : commande Mac/Linux tapée sur Windows
Solution: utiliser venv\Scripts\activate (Windows)
Cause : l'élément sélectionné est un Rectangle, pas un Frame
Solution: clic droit → "Frame selection", puis Shift+A
Cause : dark mode du navigateur
Solution: html { color-scheme: light; }
Cause : body n'est pas en flexbox
Solution:
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; }
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
Cause : virgule manquante entre les paramètres
Solution: vérifier que chaque paramètre se termine par une virgule
sauf le dernier
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
# 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:5000http://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
- 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