Qu'est-ce que MELODI ?
MELODI est un framework ERP modulaire et extensible écrit en Python. Conçu pour les applications métiers complexes (ERP, CRM, applications d'entreprise), il offre une architecture modulaire qui favorise le découplage et la maintenance à long terme.
Points Forts
- Modularité totale : Isolation stricte des responsabilités
- Performance : Core léger, chargement à la demande
- Extensibilité : Système de plugins dynamiques
- DX (Developer Experience) : API claire, conventions simples
- i18n natif : Support multi-langues intégré
Installation
Méthode Classique
# Cloner le repo
git clone https://github.com/byt3lab/MELODI.git
cd MELODI
# Environnement virtuel
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Dépendances
pip install -r requirements.txt
# Lancer
python main.py
Avec Docker
# Construction et Démarrage
docker compose up --build
# Accès
# Ouvrez http://localhost:8080
Architecture - Vue d'ensemble
MELODI repose sur une architecture en couches où le Core orchestre les Modules.
Structure du Projet
MELODI/
├── core/ # Framework (NE PAS MODIFIER)
│ ├── application.py # Singleton principal
│ ├── module/ # Système de modules
│ ├── router/ # WebRouter & APIRouter
│ ├── db/ # ORM SQLite
│ ├── component/ # Widget, Menu, HomePage
│ ├── utils/ # Event, Storage, Timer, Translation
│ └── service/ # ServiceManager
├── modules/ # VOS MODULES MÉTIERS
│ └── mon_module/
│ ├── infos.json
│ ├── module.py
│ ├── templates/
│ └── static/
├── plugins/ # Scripts dynamiques
├── config/ # Configuration
└── main.py # Point d'entrée
Classes Principales
Application: Singleton qui initialise tous les managersApplicationModule: Classe de base pour vos modulesModuleManager: Découvre et charge les modulesWebRouter / APIRouter: Gestion des routesDataBase: ORM SQLite simplifié
Cycle de Vie de l'Application
Comprendre le cycle de vie est essentiel pour un développement avancé.
- Initialisation (init) :
Applicationinstancie tous les managers (DB, Router, ModuleManager, etc.) - Build : Le
ModuleManagerscannemodules/et instancie chaque module - Load Phase : La méthode
load()de chaque module est appelée (routes, DB, services) - Run Phase :
module_manager.run_modules()active les routes - Démarrage serveur : Flask démarre et écoute les requêtes
load() s'exécute au démarrage, pas à
chaque requête. Utilisez les décorateurs de route pour la logique par requête.
Modules vs Plugins
Modules (ApplicationModule)
Les modules sont des packages Python complets avec leur propre structure, templates, et routes.
class MonModule(ApplicationModule):
def load(self):
# Routes, Services, Widgets, DB
super().load()
module = MonModule(name="Mon Module", router_name="mon_module")
Plugins (PluginModule)
Les plugins sont des scripts Python simples pour des tâches ponctuelles ou des extensions légères.
# plugins/mon_script.py
def run():
print("Plugin exécuté!")
ApplicationModule pour des fonctionnalités
complexes, un Plugin pour des scripts utilitaires.
WebRouter & APIRouter
MELODI propose deux types de routers pour séparer clairement UI et API.
WebRouter
Pour les routes retournant du HTML (pages web).
@self.router.add_route("/clients", methods=["GET"])
def liste_clients():
clients = self.app.db.select("clients")
return self.router.render_template("clients.html", clients=clients)
APIRouter
Pour les routes retournant du JSON (API REST).
@self.api_router.add_route("/clients", methods=["GET"])
def api_liste_clients():
clients = self.app.db.select("clients")
return {"status": "success", "data": clients}
- WebRouter :
/mon_module/clients - APIRouter :
/api/mon_module/clients
Fichiers Statiques
Chaque module peut avoir son propre dossier static/ pour CSS, JS, images.
Structure
mon_module/
└── static/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
Utilisation dans les templates
<link rel="stylesheet" href="/mon_module/static/css/style.css">
<script src="/mon_module/static/js/app.js"></script>
<img src="/mon_module/static/images/logo.png">
MelodiJS - Composants Réactifs
MelodiJS est un framework JavaScript réactif intégré à MELODI, similaire à Vue.js. Il permet de créer des interfaces dynamiques avec data binding, composants réutilisables et gestion d'événements.
modulename/templates/melodijs/ et exposés automatiquement via
/static_templates_melodijs/modulename/fichier.html
Architecture MelodiJS
MelodiJS suit le pattern composant avec :
- data() : État réactif du composant
- methods : Fonctions du composant
- template : HTML du composant (inline, URL, ou sélecteur)
- props : Propriétés passées au composant
- hooks : Lifecycle hooks (mounted, etc.)
Structure des Templates
mon_module/
├── templates/
│ └── mon_module/ # Templates standard Jinja2 (IMPORTANT!)
│ └── page.html
└── templates/melodijs/ # Composants MelodiJS réactifs
├── demo.html
├── demo.js
└── widget.html
modulename/templates/modulename/. Par exemple :
contacts/templates/contacts/list.html
Créer un Composant Simple
Fichier : mon_module/static/js/app.js
import { createApp } from '/static/base/melodiJS/melodijs.js'
const app = createApp({
components: {
"mon-composant": {
data() {
return {
count: 0,
message: "Hello MelodiJS!"
}
},
methods: {
incrementer() {
this.count++
}
},
template: `
<div>
<h3>{{ message }}</h3>
<p>Compteur : {{ count }}</p>
<button @click="incrementer">+1</button>
</div>
`
}
}
})
app.mount("#app")
Utiliser un Template Externe
Fichier : mon_module/templates/melodijs/compteur.html
<div class="compteur">
<h3>{{ titre }}</h3>
<p>Valeur : {{ valeur }}</p>
<button @click="incrementer">Augmenter</button>
<button @click="decrementer">Diminuer</button>
</div>
Fichier : mon_module/static/js/app.js
import { createApp } from '/static/base/melodiJS/melodijs.js'
function getUrlTemplate(template) {
return "/static_templates_melodijs/mon_module/" + template + ".html"
}
const app = createApp({
components: {
"c-compteur": {
data() {
return {
titre: "Mon Compteur",
valeur: 0
}
},
template: { url: getUrlTemplate("compteur") },
methods: {
incrementer() {
this.valeur++
},
decrementer() {
this.valeur--
}
}
}
}
})
app.mount("#app")
Props (Passage de Données)
Les props permettent de passer des données du parent vers les enfants.
Parent :
<c-user name="Gomsu Gaetant" role="Admin"></c-user>
Composant :
{
"c-user": {
props: ["name", "role"],
template: `
<div class="user-card">
<h4>{{ name }}</h4>
<p>Rôle : {{ role }}</p>
</div>
`
}
}
Composition de Composants
Les composants peuvent s'imbriquer les uns dans les autres.
// Composant parent
"c-dashboard": {
template: `
<div>
<h2>Dashboard</h2>
<c-stats></c-stats>
<c-user-list></c-user-list>
</div>
`
}
// Composants enfants
"c-stats": {
template: `<div>Statistiques...</div>`
},
"c-user-list": {
template: `<div>Liste utilisateurs...</div>`
}
Événements (@click, @submit, etc.)
<button @click="sauvegarder">Sauvegarder</button>
<form @submit="traiterFormulaire">
<input type="text" v-model="nom">
<button type="submit">Envoyer</button>
</form>
Hooks Lifecycle
{
"mon-composant": {
data() {
return { users: [] }
},
hooks: {
mounted() {
// Appelé quand le composant est monté dans le DOM
console.log("Composant monté!")
this.chargerDonnees()
}
},
methods: {
async chargerDonnees() {
const response = await fetch('/api/mon_module/users')
this.users = await response.json()
}
}
}
}
Exemple Complet : TodoList Réactive
mon_module/templates/melodijs/todolist.html
<div class="todo-app">
<h2>Ma TodoList</h2>
<form @submit="ajouterTache">
<input type="text" v-model="nouvelleTache" placeholder="Nouvelle tâche">
<button type="submit">Ajouter</button>
</form>
<ul>
<li v-for="(tache, index) in taches" :key="index">
{{ tache }}
<button @click="supprimerTache(index)">✕</button>
</li>
</ul>
</div>
mon_module/static/js/todo.js
import { createApp } from '/static/base/melodiJS/melodijs.js'
const app = createApp({
components: {
"c-todolist": {
data() {
return {
nouvelleTache: "",
taches: []
}
},
template: {
url: "/static_templates_melodijs/mon_module/todolist.html"
},
methods: {
ajouterTache(e) {
e.preventDefault()
if (this.nouvelleTache.trim()) {
this.taches.push(this.nouvelleTache)
this.nouvelleTache = ""
}
},
supprimerTache(index) {
this.taches.splice(index, 1)
}
},
hooks: {
mounted() {
console.log("TodoList montée!")
}
}
}
}
})
app.mount("#app")
Page HTML principale
<!DOCTYPE html>
<html>
<head>
<title>TodoList</title>
</head>
<body>
<div id="app">
<c-todolist></c-todolist>
</div>
<script type="module" src="/mon_module/static/js/todo.js"></script>
</body>
</html>
this.taches, le DOM se met à jour automatiquement !
Widgets (WidgetManager)
Les widgets sont des composants HTML réutilisables que vous pouvez injecter dans n'importe quelle page.
Enregistrement
@self.register_widget("stats_widget")
def stats_widget():
total = self.app.db.select("commandes")
html = f'''
<div class="widget-stats">
<h3>Statistiques</h3>
<p>Commandes : {len(total)}</p>
</div>
'''
return self.router.render_template_string(html)
Utilisation dans un template
{{ get_widget("mon_module", "stats_widget") }}
HomePage Manager
Le HomePageManager permet à chaque module de contribuer à la page d'accueil.
Enregistrement
def ma_home_page():
return self.router.render_template("home_widget.html")
self.register_home_page(ma_home_page, {"priority": 10})
Système d'Événements (EventListener)
Pour éviter le couplage fort entre modules, utilisez le système d'événements pub/sub.
Émettre un événement
# Module A
self.app.event_listener.emit("commande_creee", {
"id": 123,
"montant": 1500,
"client_id": 45
})
Écouter un événement
# Module B
def on_commande_creee(data):
print(f"Nouvelle commande #{data['id']} - {data['montant']}€")
# Envoi email, mise à jour stock, etc.
self.app.event_listener.on("commande_creee", on_commande_creee)
Services (ServiceManager)
Les services permettent de partager de la logique métier réutilisable entre modules.
Enregistrement
@self.register_service("calcul_tva")
def calcul_tva(montant_ht, taux=0.20):
return montant_ht * taux
Utilisation
# Dans n'importe quel module
tva_service = self.app.service_manager.get("mon_module", "calcul_tva")
montant_tva = tva_service(1000) # 200.0
Translation (Internationalisation)
MELODI supporte nativement plusieurs langues via le système Translation.
Structure
mon_module/
└── langs/
├── fr/
│ └── messages.json
└── en/
└── messages.json
Fichier de traduction (langs/fr/messages.json)
{
"welcome": "Bienvenue",
"goodbye": "Au revoir",
"users_count": "Nombre d'utilisateurs"
}
Initialisation dans le module
self.init_translation(default_lang="fr")
Utilisation
translations = self.translate("messages", ["welcome", "goodbye"], lang="fr")
print(translations["welcome"]) # "Bienvenue"
Storage (Gestion de fichiers)
Le Storage facilite la gestion des fichiers uploadés.
Sauvegarder un fichier
from flask import request
@self.router.add_route("/upload", methods=["POST"])
def upload_file():
file = request.files['document']
path = self.app.storage.save(file, folder="documents")
return {"path": path}
Récupérer un fichier
file_path = self.app.storage.get_path("documents/mon_fichier.pdf")
Timer (Tâches planifiées)
Le TimerManager permet d'exécuter des tâches à intervalles réguliers.
Planifier une tâche
def tache_quotidienne():
print("Nettoyage des données...")
self.app.timer_manager.schedule(tache_quotidienne, interval=86400) # 24h en secondes
Configuration
Le système de configuration centralise tous les paramètres de l'application.
Accès à la config
# Dans un module
debug_mode = self.app.config.DEBUG
db_path = self.app.config.PATH_DB
Variables disponibles
PATH_DIR_RACINE: Racine du projetPATH_DIR_MODULES: Dossier des modulesPATH_DIR_STORAGE: Dossier de stockagePATH_DB: Chemin de la base de données
Base de Données (ORM)
MELODI fournit un ORM SQLite simple mais puissant.
Créer une table
self.app.db.create_table("produits", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"nom": "TEXT NOT NULL",
"prix": "REAL",
"stock": "INTEGER DEFAULT 0",
"created_at": "DATETIME DEFAULT CURRENT_TIMESTAMP"
})
Opérations CRUD
# INSERT
self.app.db.insert("produits", {"nom": "Laptop", "prix": 999.99, "stock": 10})
# SELECT (tous)
produits = self.app.db.select("produits")
# SELECT (avec condition)
produits_chers = self.app.db.select("produits", where={"prix": ">500"})
# UPDATE
self.app.db.update("produits", {"stock": 5}, where={"id": 1})
# DELETE
self.app.db.delete("produits", where={"id": 1})
Tutoriel : Module de Gestion de Contacts
Créons un module complet avec CRUD, widgets et événements.
1. Structure
modules/
└── contacts/
├── infos.json
├── module.py
├── templates/
│ ├── list.html
│ └── form.html
└── static/
└── css/
└── contacts.css
2. infos.json
{
"version": "1.0",
"name": "contacts",
"title": "Gestion Contacts",
"description": "Module de gestion de contacts",
"depends": {
"modules": []
}
}
3. module.py (Complet)
from core.module import ApplicationModule
from flask import request, redirect
class ContactModule(ApplicationModule):
def load(self):
# Création table
self.app.db.create_table("contacts", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"nom": "TEXT NOT NULL",
"email": "TEXT",
"telephone": "TEXT",
"created_at": "DATETIME DEFAULT CURRENT_TIMESTAMP"
})
# Route liste
@self.router.add_route("/list")
def liste():
contacts = self.app.db.select("contacts")
return self.router.render_template("list.html", contacts=contacts)
# Route ajout (POST)
@self.router.add_route("/add", methods=["POST"])
def add():
data = {
"nom": request.form.get("nom"),
"email": request.form.get("email"),
"telephone": request.form.get("telephone")
}
contact_id = self.app.db.insert("contacts", data)
# Émettre événement
self.app.event_listener.emit("contact_cree", {"id": contact_id, **data})
return redirect("/contacts/list")
# Route suppression
@self.router.add_route("/delete/<int:id>")
def delete(id):
self.app.db.delete("contacts", where={"id": id})
return redirect("/contacts/list")
# Widget compteur
@self.register_widget("compteur_contacts")
def compteur():
total = len(self.app.db.select("contacts"))
return self.router.render_template_string(
f"<div class='widget'><h4>Contacts</h4><p>{total}</p></div>"
)
super().load()
module = ContactModule(name="Contacts", router_name="contacts")
4. templates/list.html
<!DOCTYPE html>
<html>
<head>
<title>Contacts</title>
<link rel="stylesheet" href="/contacts/static/css/contacts.css">
</head>
<body>
<h1>Liste des Contacts</h1>
<form action="/contacts/add" method="POST">
<input type="text" name="nom" placeholder="Nom" required>
<input type="email" name="email" placeholder="Email">
<input type="tel" name="telephone" placeholder="Téléphone">
<button type="submit">Ajouter</button>
</form>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr>
<td>{{ contact.nom }}</td>
<td>{{ contact.email }}</td>
<td>{{ contact.telephone }}</td>
<td>
<a href="/contacts/delete/{{ contact.id }}">Supprimer</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
Tutoriel : Module Blog avec i18n
Un module blog avec support multilingue.
Structure avec traductions
modules/
└── blog/
├── infos.json
├── module.py
├── langs/
│ ├── fr/
│ │ └── messages.json
│ └── en/
│ └── messages.json
└── templates/
└── index.html
langs/fr/messages.json
{
"title": "Blog",
"new_post": "Nouvel article",
"published": "Publié le",
"no_posts": "Aucun article pour le moment"
}
module.py avec traductions
class BlogModule(ApplicationModule):
def load(self):
# Initialiser traductions
self.init_translation(default_lang="fr")
self.app.db.create_table("posts", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"title": "TEXT NOT NULL",
"content": "TEXT",
"published_at": "DATETIME DEFAULT CURRENT_TIMESTAMP"
})
@self.router.add_route("/")
def index():
posts = self.app.db.select("posts")
lang = request.args.get("lang", "fr")
t = self.translate("messages", ["title", "new_post", "no_posts"], lang=lang)
return self.router.render_template("index.html", posts=posts, t=t)
super().load()
module = BlogModule(name="Blog", router_name="blog")
API Reference - Application
Singleton central de MELODI. Accessible via self.app dans les
modules.
Attributs
server: Instance FlaskAdapterconfig: Configuration globaledb: Instance DataBaserouter: WebRouter principalapi_router: APIRouter principalmodule_manager: Gestionnaire de modulesservice_manager: Gestionnaire de serviceswidget_manager: Gestionnaire de widgetsmenu_item_manager: Gestionnaire de menushome_page_manager: Gestionnaire de homepageevent_listener: Système d'événementstimer_manager: Gestionnaire de timersstorage: Gestionnaire de fichiers
Méthodes
-
init() -> NoneInitialise tous les managers.
-
build() -> NoneCharge et initialise tous les modules.
-
run(host="0.0.0.0", port=5000, debug=True) -> NoneDémarre le serveur Flask.
API Reference - ApplicationModule
Classe de base pour tous les modules MELODI.
Attributs
app: Référence à Applicationrouter: WebRouter du moduleapi_router: APIRouter du moduledirname: Nom du dossier du moduletranslation: Instance Translation (si initialisée)
Méthodes
-
load() -> NoneÀ surcharger. Point d'entrée pour définir routes, services, widgets. Appelez toujours
super().load()à la fin. -
register_service(name: str)Décorateur pour enregistrer un service.
-
register_widget(name: str, infos: dict = {})Décorateur pour enregistrer un widget.
-
register_home_page(func, infos: dict)Enregistre un composant de homepage.
-
init_translation(default_lang: str) -> NoneInitialise le système de traduction.
-
translate(filename: str|list, keys: str|list, lang: str = None) -> dictRécupère des traductions.
API Reference - Managers
-
create_table(name: str, schema: dict) -> NoneCrée une table si elle n'existe pas.
-
select(table: str, where: dict = None) -> listSélectionne des enregistrements.
-
insert(table: str, data: dict) -> intInsère un enregistrement. Retourne l'ID.
-
update(table: str, data: dict, where: dict) -> NoneMet à jour des enregistrements.
-
delete(table: str, where: dict) -> NoneSupprime des enregistrements.
-
register(name_module: str, name_widget: str, widget, infos: dict) -> NoneEnregistre un widget.
-
get(name_module: str, name_widget: str) -> callableRécupère un widget.
-
render(name_module: str, name_widget: str) -> strRend un widget (HTML).
-
register(name_module: str, name_service: str, service) -> NoneEnregistre un service.
-
get(name_module: str, name_service: str) -> callableRécupère un service.
-
emit(event_name: str, data: dict) -> NoneÉmet un événement.
-
on(event_name: str, callback: callable) -> NoneÉcoute un événement.