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.

Philosophie : Contrairement aux frameworks monolithiques, MELODI adopte une approche où chaque fonctionnalité métier est encapsulée dans un module autonome. Vous pouvez ajouter, supprimer ou modifier des modules sans toucher au core.

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 managers
  • ApplicationModule : Classe de base pour vos modules
  • ModuleManager : Découvre et charge les modules
  • WebRouter / APIRouter : Gestion des routes
  • DataBase : ORM SQLite simplifié

Cycle de Vie de l'Application

Comprendre le cycle de vie est essentiel pour un développement avancé.

  1. Initialisation (init) : Application instancie tous les managers (DB, Router, ModuleManager, etc.)
  2. Build : Le ModuleManager scanne modules/ et instancie chaque module
  3. Load Phase : La méthode load() de chaque module est appelée (routes, DB, services)
  4. Run Phase : module_manager.run_modules() active les routes
  5. Démarrage serveur : Flask démarre et écoute les requêtes
Attention : Toute logique dans 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é!")
Conseil : Utilisez un 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}
URLs :
  • 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.

Templates MelodiJS : Les composants MelodiJS sont stockés dans 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
Attention : Les templates Jinja2 doivent être dans 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>
Astuce : MelodiJS gère automatiquement la réactivité. Quand vous modifiez 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 projet
  • PATH_DIR_MODULES : Dossier des modules
  • PATH_DIR_STORAGE : Dossier de stockage
  • PATH_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

class core.application.Application

Singleton central de MELODI. Accessible via self.app dans les modules.

Attributs

  • server : Instance FlaskAdapter
  • config : Configuration globale
  • db : Instance DataBase
  • router : WebRouter principal
  • api_router : APIRouter principal
  • module_manager : Gestionnaire de modules
  • service_manager : Gestionnaire de services
  • widget_manager : Gestionnaire de widgets
  • menu_item_manager : Gestionnaire de menus
  • home_page_manager : Gestionnaire de homepage
  • event_listener : Système d'événements
  • timer_manager : Gestionnaire de timers
  • storage : Gestionnaire de fichiers

Méthodes

  • init() -> None
    Initialise tous les managers.
  • build() -> None
    Charge et initialise tous les modules.
  • run(host="0.0.0.0", port=5000, debug=True) -> None
    Démarre le serveur Flask.

API Reference - ApplicationModule

class core.module.ApplicationModule

Classe de base pour tous les modules MELODI.

Attributs

  • app : Référence à Application
  • router : WebRouter du module
  • api_router : APIRouter du module
  • dirname : Nom du dossier du module
  • translation : 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) -> None
    Initialise le système de traduction.
  • translate(filename: str|list, keys: str|list, lang: str = None) -> dict
    Récupère des traductions.

API Reference - Managers

class core.db.DataBase
  • create_table(name: str, schema: dict) -> None
    Crée une table si elle n'existe pas.
  • select(table: str, where: dict = None) -> list
    Sélectionne des enregistrements.
  • insert(table: str, data: dict) -> int
    Insère un enregistrement. Retourne l'ID.
  • update(table: str, data: dict, where: dict) -> None
    Met à jour des enregistrements.
  • delete(table: str, where: dict) -> None
    Supprime des enregistrements.
class core.component.WidgetManager
  • register(name_module: str, name_widget: str, widget, infos: dict) -> None
    Enregistre un widget.
  • get(name_module: str, name_widget: str) -> callable
    Récupère un widget.
  • render(name_module: str, name_widget: str) -> str
    Rend un widget (HTML).
class core.service.ServiceManager
  • register(name_module: str, name_service: str, service) -> None
    Enregistre un service.
  • get(name_module: str, name_service: str) -> callable
    Récupère un service.
class core.utils.EventListener
  • emit(event_name: str, data: dict) -> None
    Émet un événement.
  • on(event_name: str, callback: callable) -> None
    Écoute un événement.