Framework web MVC pur Python, HTTPS natif, Jinja2 intégré.
Runtime léger : trois dépendances Python explicites — MariaDB, python-dotenv et Jinja2.
Copyright (c) 2026 Roger Cauchon — voir LICENSE
Forge suit une règle stricte de séparation entre ce que le framework fournit et ce que l'application implémente.
| Outil | Rôle |
|---|---|
core/http/request.py |
Encapsulation de la requête HTTP |
core/http/response.py |
Réponse HTTP |
core/http/helpers.py |
Helper html() — rendu Jinja2 → Response |
core/http/router.py |
Routage statique/dynamique, groupes, noms |
core/application.py |
Pipeline middlewares + dispatch + CSRF auto + 500 auto |
core/templating/ |
Contrat Renderer + singleton template_manager |
core/security/session.py |
Sessions, CSRF, expiration |
core/security/hashing.py |
PBKDF2-HMAC-SHA256 + rate limiting |
core/security/middleware.py |
AuthMiddleware, CsrfMiddleware |
core/security/decorators.py |
@require_auth, @require_csrf, @require_role |
core/forms/ |
Form, Field, cleaned_data, erreurs affichables |
core/mvc/controller/base_controller.py |
render, redirect, json, body, csrf_token… |
core/mvc/model/validator.py |
Logique de validation seule (sans HTML) |
core/mvc/view/pagination.py |
Calcul de pagination |
core/forge.py |
Registre de configuration du noyau |
| Composant | Rôle |
|---|---|
mvc/routes.py |
Déclaration des routes de votre application |
mvc/controllers/ |
Contrôleurs métier |
mvc/models/ |
Requêtes SQL de votre base de données |
mvc/forms/ |
Formulaires applicatifs |
mvc/validators/ |
Règles de validation de vos entités |
mvc/helpers/form_errors.py |
Rendu HTML des erreurs |
mvc/helpers/flash.py |
Rendu HTML des messages flash |
mvc/views/ |
Tous les templates Jinja2, y compris login et layout |
Le modèle d'entités officiel repose sur mvc/entities/.
mvc/
└── entities/
├── relations.json
├── relations.sql
└── contact/
├── __init__.py
├── contact.json
├── contact.sql
├── contact_base.py
└── contact.py
Rôle des fichiers :
contact.json: source canonique locale de l'entitécontact.sql: projection SQL locale régénérablecontact_base.py: base Python générée régénérablecontact.py: classe métier manuelle finalerelations.json: source canonique globale des relationsrelations.sql: projection SQL globale des relations
Le framework ne connaît pas votre schéma de base, ne sait pas qui s'appelle
loginoupassword, et n'impose aucune route par défaut.AuthController, les vues de connexion et les routes/login//logoutsont du code applicatif fourni à titre d'exemple dansmvc/— vous pouvez les modifier ou les supprimer librement.
Forge vise un CRUD applicatif complet, explicite et lisible, sans ORM implicite : formulaires, validation, CSRF automatique, messages flash, redirections, erreurs de formulaire et modèles applicatifs SQL structurés.
Doctrine associée :
- Forge ne génère pas de repository magique.
- Forge ne cache pas le SQL.
- Forge fournit une structure stable pour organiser le CRUD.
- Le développeur reste propriétaire du modèle applicatif.
| Outil | Version minimale |
|---|---|
| Python | 3.11 ou supérieur ; 3.12.x recommandé pour développer Forge |
| MariaDB | 10.6 |
| OpenSSL | disponible dans le terminal |
| Node.js | 20, optionnel — uniquement pour recompiler Tailwind CSS |
Tailwind est le framework CSS officiel de Forge pour les templates générés. Forge ne maintient pas plusieurs variantes Bootstrap, Bulma, Foundation ou autres frameworks CSS.
Le workflow front standard est :
npm install
npm run build:cssLe fichier source est static/src/input.css. Le fichier compilé servi par
l'application est static/tailwind.css.
Node.js/npm est utile pour recompiler le CSS, mais il n'est pas nécessaire pour
exécuter le serveur Python Forge quand static/tailwind.css existe déjà.
Un développeur peut remplacer Tailwind manuellement dans son application, hors
chemin standard généré par Forge. Voir docs/front.md.
Forge est publié sur PyPI sous le nom forge-mvc.
Sous Linux Ubuntu / Zorin :
sudo apt update
sudo apt install -y git python3 python3-venv python3-pip pipx openssl mariadb-server build-essential python3-dev libmariadb-dev pkg-config
pipx ensurepathFermez puis rouvrez le terminal si pipx ensurepath le demande.
pipx install forge-mvc
forge --versionforge new NomDuProjet
cd NomDuProjetRemplacez NomDuProjet par le nom de votre application.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txtcp env/example env/devPuis éditez env/dev avec vos paramètres MariaDB.
openssl req -x509 -newkey rsa:2048 \
-keyout key.pem \
-out cert.pem \
-days 365 \
-nodes \
-subj "/CN=localhost"forge db:init
python app.pyPuis ouvrir dans le navigateur :
https://localhost:8000
L’installation manuelle reste utile pour contribuer au framework, tester une branche précise ou travailler directement sur le code source de Forge.
Sous Linux Ubuntu / Zorin :
sudo apt update
sudo apt install -y git python3 python3-venv python3-pip openssl mariadb-server build-essential python3-dev libmariadb-dev pkg-configgit clone --branch v1.5.0 --depth=1 https://github.com/caucrogeGit/Forge.git NomDuProjet
cd NomDuProjetRemplacez NomDuProjet par le nom de votre application.
Le clone sur un tag laisse le dépôt en detached HEAD. Réinitialisez-le immédiatement pour démarrer votre propre historique :
rm -rf .git
git init
git add -A
git commit -m "init: NomDuProjet — based on Forge 1.5.0"
forge new NomDuProjetfait ces étapes automatiquement et reste la voie recommandée.
python3 -m venv .venv
source .venv/bin/activate # Windows : .venv\Scripts\activate
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .La commande
forgelocale n'est disponible qu'après installation du package en mode editable. Sanspip install -e ., la commandeforgedu projet ne sera pas trouvée.
openssl req -x509 -newkey rsa:2048 \
-keyout key.pem \
-out cert.pem \
-days 365 \
-nodes \
-subj "/CN=localhost"cp env/example env/devPuis éditez env/dev avec vos paramètres MariaDB.
Exemple :
APP_NAME=Forge
APP_ROUTES_MODULE=mvc.routes
# Administration MariaDB globale
DB_ADMIN_HOST=localhost
DB_ADMIN_PORT=3306
DB_ADMIN_LOGIN=root
DB_ADMIN_PWD=
# Base projet
DB_NAME=contacts
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# Utilisateur applicatif du projet
DB_APP_HOST=localhost
DB_APP_PORT=3306
DB_APP_LOGIN=forge
DB_APP_PWD=motdepassefort
DB_POOL_SIZE=5
APP_HOST=127.0.0.1
APP_PORT=8000
APP_SSL_ENABLED=true
# Dev : HTTPS local. Prod derrière Nginx : APP_SSL_ENABLED=false.
SSL_CERTFILE=cert.pem
SSL_KEYFILE=key.pemLe flux recommandé est désormais :
forge db:initCette commande utilise DB_ADMIN_* pour préparer DB_NAME, créer DB_APP_LOGIN si nécessaire et attribuer les droits sur la base du projet.
Elle prépare aussi la table technique forge_migrations, utilisée par les
migrations SQL versionnées.
Contrat de la commande :
- si la base existe déjà,
forge db:initne tombe pas en erreur et signale simplement qu’elle est déjà présente - si l’utilisateur applicatif existe déjà,
forge db:initne le recrée pas et ne modifie pas silencieusement son mot de passe - les privilèges sur la base projet sont appliqués ou réappliqués à chaque exécution
- si la situation de l’utilisateur existant est ambiguë ou non vérifiable,
forge db:initdemande une vérification manuelle au lieu de “réparer” silencieusement
Politique de privilèges par défaut sur DB_NAME.* :
SELECTINSERTUPDATEDELETECREATEALTERDROPINDEXREFERENCES
Forge n’accorde pas de privilèges globaux serveur à l’utilisateur applicatif et n’utilise pas ALL PRIVILEGES par défaut si cette liste explicite suffit.
Développement vs production
En développement,
forge db:initconserve un flux pédagogique simple : le compteDB_APP_LOGINreçoit aussi les droits nécessaires àforge db:apply(CREATE,ALTER,DROP,INDEX,REFERENCES). C'est pratique en développement et pour les starters.En production, privilégiez une séparation stricte : un compte d'administration ou de migration pour
forge db:init/forge db:apply, et un compte applicatif runtime limité àSELECT,INSERT,UPDATE,DELETE.Cette séparation sera formalisée dans une évolution ultérieure sans changer la doctrine JSON/SQL actuelle.
Les migrations SQL versionnées vivent dans mvc/migrations/. Le flux complet
est documenté dans docs/migrations.md :
forge migration:make initial_schema --from-entities
forge migration:status
forge migration:applypython app.pyPuis ouvrir dans le navigateur :
https://localhost:8000
Si vous utilisez encore l'ancien outillage de préparation de schéma et de sécurité, les commandes suivantes existent toujours :
python cmd/make.py schema:create
python cmd/make.py security:init --env devElles restent techniques, internes et hors flux officiel du modèle d'entités.
La méthode recommandée pour créer un projet est désormais
pipx install forge-mvc, puisforge new NomDuProjet. L’installation manuelle reste réservée au développement du framework ou aux tests sur une version Git précise.
forge doctor
forge make:entity Contact
forge sync:entity Contact
forge sync:relations
forge build:model
forge check:model
forge db:init
forge upload:init
forge media:init
forge mail:init
forge db:apply
forge routes:listCycle recommandé :
forge doctorpour vérifier l’environnement avant de démarrerforge make:entity Contact- édition de
mvc/entities/contact/contact.json forge sync:entity Contactforge check:modelforge build:modelforge db:initforge upload:initsi votre application reçoit des fichiersforge media:initsi votre application utilise le socle média et les variantes d’imagesforge mail:initsi votre application envoie des mailsforge db:applyforge routes:listpour vérifier le routage déclaré
L'ancien outillage cmd/make.py existe toujours, mais reste hors flux officiel pour le modèle d'entités.
.
├── app.py # Point d'entrée — serveur HTTPS, routeur
├── forge.py # Point d’entrée CLI officiel
├── config.py # Chargement des variables d'environnement
│
├── core/ # Framework — ne pas modifier
│ ├── application.py # Dispatcher : middlewares + routage + 500 automatique
│ ├── forge.py # Registre de configuration du noyau
│ ├── http/
│ │ ├── request.py # Requête HTTP (form, JSON, params, ip)
│ │ ├── response.py # Réponse HTTP
│ │ └── helpers.py # html() — rendu Jinja2 → Response
│ ├── templating/
│ │ ├── contracts.py # Protocole Renderer (swappable)
│ │ └── manager.py # Singleton template_manager
│ ├── database/
│ │ ├── connection.py # Pool de connexions MariaDB thread-safe
│ │ └── sql_loader.py # Chargement des requêtes selon APP_ENV
│ ├── forms/ # Form, Field, cleaned_data, erreurs
│ ├── mvc/
│ │ ├── controller/
│ │ │ └── base_controller.py # render, redirect, json, body, flash, CSRF…
│ │ ├── model/
│ │ │ ├── validator.py # Validation de formulaires (logique seule)
│ │ │ └── exceptions.py
│ │ └── view/
│ │ └── pagination.py
│ └── security/
│ ├── session.py # Sessions, CSRF, expiration
│ ├── hashing.py # PBKDF2-HMAC-SHA256 + rate limiting
│ ├── middleware.py # AuthMiddleware, CsrfMiddleware
│ └── decorators.py # @require_auth, @require_csrf, @require_role
│
├── integrations/
│ └── jinja2/
│ └── renderer.py # Jinja2Renderer (autoescape HTML)
│
├── mvc/ # Application — périmètre utilisateur
│ ├── routes.py # Table de routage URL → contrôleur
│ ├── controllers/
│ ├── entities/ # Modèle canonique des entités
│ ├── forms/
│ ├── models/
│ │ └── sql/dev/ # Requêtes SQL ignorées par git
│ ├── validators/
│ ├── helpers/
│ │ ├── form_errors.py # render_errors_html
│ │ └── flash.py # render_flash_html
│ └── views/
│ ├── layouts/base.html # Gabarit commun ({% block contenu %})
│ ├── home/index.html # Page d'accueil publique
│ ├── auth/login.html
│ ├── errors/ # 403, 404, 429, 500
│ └── partials/flash.html
│
├── tests/ # Suite de tests pytest
│ ├── conftest.py # Fixtures : configure_forge_kernel, fake_request…
│ ├── fake_request.py # FakeRequest — requête simulée pour tests contrôleurs
│ ├── test_application.py # dispatch(), exceptions, pipeline middleware
│ ├── test_hashing.py
│ ├── test_json.py # BaseController.json(), json_body, FakeRequest
│ ├── test_middleware.py
│ ├── test_response.py
│ ├── test_router.py
│ ├── test_session.py
│ ├── test_templating.py # TemplateManager, Jinja2Renderer, html(), vues réelles
│ └── test_validator.py
│
├── cmd/ # Outillage CLI historique
│ ├── make.py # Point d'entrée : python cmd/make.py <commande>
│ ├── mvc/ # Générateurs MVC
│ ├── sql/ # Générateurs SQL
│ ├── inspect/ # Diagnostic
│ └── security/ # Initialisation sécurité
│
├── static/
│ ├── favicon.svg
│ ├── img/ # Logos et images
│ ├── tailwind.css # CSS compilé ignoré par git
│ └── src/input.css # Source Tailwind
│
└── env/ # Variables d'environnement
├── example # Squelette commité
├── dev # Valeurs de développement ignorées par git
└── prod # Valeurs de production ignorées par git
Navigateur
↓ HTTPS
ThreadingHTTPServer + ssl.SSLContext
↓
RequestHandler (GET / POST / PUT / PATCH / DELETE)
↓ encapsulation
Request (method, path, headers, params, body, json_body, ip)
↓
Application.dispatch()
├─ route absente → 404
├─ route protégée → pipeline middlewares
├─ méthode unsafe → CSRF automatique sauf csrf=False
└─ handler
↓ exception non gérée → 500 automatique
Contrôleur → Modèle → MariaDB
↓
html(template, context) ou BaseController.json(data) → Response
↓
Navigateur
Application orchestre le routage et les middlewares.
# Usage minimal — AuthMiddleware par défaut
app = Application(router)
# Middlewares personnalisés
app = Application(router, middlewares=[AuthMiddleware("/login"), MonMiddleware()])
# Login URL personnalisée
app = Application(router, login_url="/connexion")Un middleware est un objet exposant check(request) → Response | None.
Le premier middleware qui retourne une Response court-circuite la chaîne.
Les middlewares ne s'appliquent qu'aux routes protégées (public=False).
La protection CSRF s'applique aux méthodes unsafe (POST, PUT, PATCH, DELETE)
par défaut, y compris sur les routes publiques comme /login.
Les API et webhooks doivent demander l'exemption explicitement avec csrf=False.
Toute exception non gérée dans un contrôleur est interceptée par dispatch()
et produit automatiquement une réponse errors/500.html.
Organisation recommandée :
mvc/
├── forms/
│ └── contact_form.py
├── models/
│ ├── contact_model.py
│ └── sql/dev/contact_queries.py
└── views/contacts/
├── index.html
├── create.html
├── edit.html
└── show.html
Le contrôleur orchestre.
Le formulaire valide.
Le modèle applicatif SQL appelle des requêtes visibles dans *_queries.py.
Aucun repository généré, aucun SQL caché.
form = ContactForm.from_request(request)
if not form.is_valid():
return BaseController.validation_error(
"contacts/create.html",
context={"form": form, **form.context},
request=request,
)
contact_id = ContactModel.create(form.cleaned_data)
return BaseController.redirect_to_route(
"contacts_show",
id=contact_id,
request=request,
flash="Contact créé.",
)Pour un pivot explicite simple, RelatedIdsField prépare seulement la sélection :
class ContactForm(Form):
nom = StringField(required=True)
groupe_ids = RelatedIdsField(required=False, allowed_ids_key="allowed_group_ids")
form = ContactForm.from_request(
request,
allowed_group_ids=GroupeModel.allowed_ids(),
)Le formulaire ne persiste rien.
Le modèle applicatif SQL reste responsable de la table pivot.
Pour une écriture multiple :
from core.database.transaction import transaction
with transaction() as tx:
contact_id = ContactModel.create(form.cleaned_data, tx=tx)
ContactGroupeModel.replace_for_contact(
contact_id,
form.cleaned_data["groupe_ids"],
tx=tx,
)Dans un template :
<a href="{{ url_for('contacts_show', id=contact.Id) }}">Voir</a>
<input name="nom" value="{{ form.value('nom') }}"># Retourner du JSON depuis un contrôleur
return BaseController.json({"id": 1, "nom": "Dupont"})
return BaseController.json({"erreur": "non trouvé"}, status=404)
# Lire un body JSON (POST/PUT/PATCH/DELETE application/json)
data = BaseController.json_body(request) # → dictRequest.json_body est peuplé automatiquement si le Content-Type de la
requête est application/json.
Request.body reste le dictionnaire formulaire habituel pour POST, PUT,
PATCH et DELETE.
Forge utilise Jinja2 avec autoescape HTML activé sur tous les fichiers .html.
# Initialisation au démarrage dans app.py
from integrations.jinja2.renderer import Jinja2Renderer
from core.templating.manager import template_manager
template_manager.register(Jinja2Renderer(forge.get("views_dir")))# Dans un contrôleur
return BaseController.render(
"contacts/index.html",
context={"contacts": contacts},
request=request,
)<!-- Template Jinja2 -->
{% extends "layouts/base.html" %}
{% block contenu %}
{% for contact in contacts %}
<p>{{ contact.nom }}</p>
{% endfor %}
{% endblock %}Les variables sont échappées automatiquement contre le XSS.
Utilisez {{ variable | safe }} uniquement pour du HTML pré-rendu contrôlé
comme les messages flash ou les erreurs de formulaire.
Forge est un framework intentionnellement minimal. Ces limites sont des choix, non des dettes techniques.
| Forge ne fournit pas | Alternative si besoin |
|---|---|
| ORM ou query builder | Requêtes SQL paramétrées directes |
| Ancien outillage CLI | cmd/make.py existe encore, mais reste hors flux officiel pour les entités |
| Backend de session persistant | Sessions en mémoire — remplacez _sessions dans session.py |
| Routing avancé | Le routeur actuel couvre les besoins CRUD courants |
| Gestion des rôles intégrée | @require_role + table utilisateur_role dans les applications ou starters |
| Support multi-base | Un connecteur MariaDB — ajoutez le vôtre si besoin |
| Rechargement automatique | Lancez avec watchdog ou un process manager |
| Système de plugins | Architecture directe — étendez sans couche d'abstraction inutile |
pip install -r requirements-dev.txt
python -m pytest tests/ -vLes tests ne nécessitent pas MariaDB installé : l'import du driver est
paresseux (core/database/connection.py) et les modèles DB sont mockés dans les
tests applicatifs.
Pour tester un contrôleur sans démarrer le serveur :
from tests.fake_request import FakeRequest
req = FakeRequest("GET", "/clients")
req = FakeRequest("POST", "/clients", body={"Nom": "Dupont"})
req = FakeRequest("POST", "/api/sync", json_body={"ids": [1, 2]})
req = FakeRequest("GET", "/tableau-de-bord", session_id="abc123")Forge stocke les sessions en mémoire dans le processus Python.
Ce choix est adapté au développement, à la pédagogie et aux petites applications mono-processus.
Limites connues :
- les sessions sont perdues au redémarrage ;
- elles ne sont pas partagées entre plusieurs workers ou plusieurs machines ;
- ce stockage n'est pas adapté au scaling horizontal.
Pour une production multi-processus, remplacez le stockage mémoire par un backend persistant compatible avec vos contraintes d'exploitation.
| Mesure | Détail |
|---|---|
| HTTPS | ssl.SSLContext sur ThreadingHTTPServer |
| Authentification | Cookie HttpOnly; Secure; SameSite=Strict + vérification en base |
| CSRF | Token par session, vérifié sur POST, PUT, PATCH, DELETE sauf csrf=False explicite |
| XSS | Autoescape Jinja2 sur tous les templates .html |
| Mots de passe | PBKDF2-HMAC-SHA256, 260 000 itérations, sel aléatoire |
| Timing attacks | hmac.compare_digest() |
| Session fixation | Nouveau session_id après chaque connexion |
| Rate limiting | 5 tentatives / 60 s par IP sur /login |
| Headers HTTP | CSP, HSTS, X-Frame-Options, X-Content-Type-Options |
| Path traversal | os.path.realpath() sur les fichiers statiques |
| Injection SQL | Requêtes paramétrées exclusivement |
Runtime (requirements.txt)
| Package | Rôle |
|---|---|
mariadb |
Connecteur MariaDB natif |
python-dotenv |
Chargement des fichiers env/* |
jinja2 |
Moteur de templates avec autoescape HTML |
Développement (requirements-dev.txt)
| Package | Rôle |
|---|---|
pytest |
Suite de tests |
La landing page actuelle est une solution transitoire.
Elle fonctionne et remplit son rôle comme page d'accueil publique par défaut de Forge.
Elle est servie comme template Jinja2 et repose sur les assets locaux du projet.
Elle n'utilise plus React UMD, Babel standalone ni dépendance CDN pour son rendu.
La source canonique de la landing est :
mvc/views/landing/index.html
La page d'accueil MkDocs docs/index.html est générée depuis cette source :
forge sync:landing
forge sync:landing --checkNe modifiez pas docs/index.html à la main.
Forge est distribué sous licence propriétaire / source disponible.
L'usage professionnel, commercial ou institutionnel n'est pas autorisé sans accord écrit préalable de Roger Cauchon.
Les usages autorisés sans accord écrit sont limités à la lecture, l'étude, l'évaluation personnelle et l'usage éducatif non commercial.
Voir LICENSE pour les conditions complètes.