Files
orion/docs/development/creating-modules.md
Samir Boulahtit 7a9dda282d refactor(scripts): reorganize scripts/ into seed/ and validate/ subfolders
Move 9 init/seed scripts into scripts/seed/ and 7 validation scripts
(+ validators/ subfolder) into scripts/validate/ to reduce clutter in
the root scripts/ directory. Update all references across Makefile,
CI/CD configs, pre-commit hooks, docs (~40 files), and Python imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:35:53 +01:00

560 lines
15 KiB
Markdown

# Creating Modules
This guide explains how to create new **plug-and-play modules** for the Wizamart platform. Modules are fully self-contained and automatically discovered - no changes to framework files required.
## Quick Start (5 Minutes)
Creating a new module requires **zero changes** to `main.py`, `registry.py`, or any other framework file.
### Step 1: Create Directory Structure
```bash
# Create module with all directories
MODULE_NAME=mymodule
mkdir -p app/modules/$MODULE_NAME/{routes/{api,pages},services,models,schemas,templates/$MODULE_NAME/store,static/store/js,locales,tasks}
# Create required files
touch app/modules/$MODULE_NAME/__init__.py
touch app/modules/$MODULE_NAME/definition.py
touch app/modules/$MODULE_NAME/exceptions.py
touch app/modules/$MODULE_NAME/routes/__init__.py
touch app/modules/$MODULE_NAME/routes/api/__init__.py
touch app/modules/$MODULE_NAME/routes/pages/__init__.py
touch app/modules/$MODULE_NAME/services/__init__.py
touch app/modules/$MODULE_NAME/models/__init__.py
touch app/modules/$MODULE_NAME/schemas/__init__.py
touch app/modules/$MODULE_NAME/tasks/__init__.py
```
### Step 2: Create Module Definition
```python
# app/modules/mymodule/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
mymodule_module = ModuleDefinition(
code="mymodule",
name="My Module",
description="Description of what this module does",
version="1.0.0",
# Classification
is_core=False, # True = always enabled
is_internal=False, # True = admin-only
is_self_contained=True,
# Features for tier-based gating
features=["mymodule_feature"],
# Menu items
menu_items={
FrontendType.STORE: ["mymodule"],
},
# Paths (for self-contained modules)
services_path="app.modules.mymodule.services",
models_path="app.modules.mymodule.models",
schemas_path="app.modules.mymodule.schemas",
exceptions_path="app.modules.mymodule.exceptions",
templates_path="templates",
locales_path="locales",
)
```
### Step 3: Create Routes (Auto-Discovered)
```python
# app/modules/mymodule/routes/api/store.py
from fastapi import APIRouter, Depends
from app.api.deps import get_current_store_api, get_db
router = APIRouter() # MUST be named 'router'
@router.get("")
def get_mymodule_data(current_user=Depends(get_current_store_api)):
return {"message": "Hello from mymodule"}
```
```python
# app/modules/mymodule/routes/pages/store.py
from fastapi import APIRouter, Request, Path
from fastapi.responses import HTMLResponse
from app.api.deps import get_current_store_from_cookie_or_header, get_db, Depends
from app.templates_config import templates
router = APIRouter() # MUST be named 'router'
@router.get("/{store_code}/mymodule", response_class=HTMLResponse)
async def mymodule_page(
request: Request,
store_code: str = Path(...),
current_user=Depends(get_current_store_from_cookie_or_header),
db=Depends(get_db),
):
return templates.TemplateResponse(
"mymodule/store/index.html",
{"request": request, "store_code": store_code},
)
```
### Step 4: Done!
That's it! The framework automatically:
- Discovers and registers the module
- Mounts API routes at `/api/v1/store/mymodule`
- Mounts page routes at `/store/{code}/mymodule`
- Loads templates from `templates/`
- Mounts static files at `/static/modules/mymodule/`
- Loads translations from `locales/`
## Complete Module Structure
```
app/modules/mymodule/
├── __init__.py # Package marker
├── definition.py # ModuleDefinition (REQUIRED)
├── config.py # Environment config (optional, auto-discovered)
├── exceptions.py # Module exceptions
├── routes/ # Auto-discovered routes
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── admin.py # /api/v1/admin/mymodule
│ │ └── store.py # /api/v1/store/mymodule
│ └── pages/
│ ├── __init__.py
│ ├── admin.py # /admin/mymodule
│ └── store.py # /store/{code}/mymodule
├── services/ # Business logic
│ ├── __init__.py
│ └── mymodule_service.py
├── models/ # SQLAlchemy models
│ ├── __init__.py
│ └── mymodel.py
├── schemas/ # Pydantic schemas
│ ├── __init__.py
│ └── myschema.py
├── templates/ # Jinja2 templates (auto-loaded)
│ └── mymodule/
│ ├── admin/
│ │ └── index.html
│ └── store/
│ └── index.html
├── static/ # Static files (auto-mounted)
│ ├── admin/
│ │ └── js/
│ │ └── mymodule.js
│ └── store/
│ └── js/
│ └── mymodule.js
├── locales/ # Translations (auto-loaded)
│ ├── en.json
│ ├── de.json
│ ├── fr.json
│ └── lu.json
├── tasks/ # Celery tasks (auto-discovered)
│ ├── __init__.py # REQUIRED for discovery
│ └── background_tasks.py
└── migrations/ # Alembic migrations (auto-discovered)
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
└── mymodule_001_initial.py
```
## Auto-Discovery Details
### What Gets Auto-Discovered
| Component | Location | Discovered By | Notes |
|-----------|----------|---------------|-------|
| Module Definition | `definition.py` | `app/modules/discovery.py` | Must export ModuleDefinition |
| Configuration | `config.py` | `app/modules/config.py` | Must export `config` or `config_class` |
| API Routes | `routes/api/*.py` | `app/modules/routes.py` | Must export `router` |
| Page Routes | `routes/pages/*.py` | `app/modules/routes.py` | Must export `router` |
| Templates | `templates/` | `app/templates_config.py` | Use namespace prefix |
| Static Files | `static/` | `main.py` | Mounted at `/static/modules/{code}/` |
| Locales | `locales/*.json` | `app/utils/i18n.py` | Keyed by module code |
| Tasks | `tasks/` | `app/modules/tasks.py` | Needs `__init__.py` |
| Migrations | `migrations/versions/` | `app/modules/migrations.py` | Use naming convention |
### Route File Requirements
Route files MUST export a `router` variable:
```python
# CORRECT - will be discovered
router = APIRouter()
@router.get("/")
def my_endpoint():
pass
# WRONG - won't be discovered
my_router = APIRouter() # Variable not named 'router'
```
### Template Namespacing
Templates must be namespaced under the module code:
```
templates/
└── mymodule/ # Module code as namespace
└── store/
└── index.html
```
Reference in code:
```python
templates.TemplateResponse("mymodule/store/index.html", {...})
```
### Static File URLs
Static files are mounted at `/static/modules/{module_code}/`:
```html
<!-- In template -->
<script src="{{ url_for('mymodule_static', path='store/js/mymodule.js') }}"></script>
```
### Translation Keys
Translations are namespaced under the module code:
```json
// locales/en.json
{
"mymodule": {
"title": "My Module",
"description": "Module description"
}
}
```
Use in templates:
```html
{{ _('mymodule.title') }}
```
## Module Classification
### Core Modules
Always enabled, cannot be disabled.
```python
ModuleDefinition(
code="mymodule",
is_core=True,
# ...
)
```
### Optional Modules (Default)
Can be enabled/disabled per platform.
```python
ModuleDefinition(
code="mymodule",
is_core=False,
is_internal=False,
# ...
)
```
### Internal Modules
Admin-only tools, not visible to stores.
```python
ModuleDefinition(
code="mymodule",
is_internal=True,
# ...
)
```
## Module Dependencies
Declare dependencies in the definition:
```python
ModuleDefinition(
code="orders",
requires=["payments"], # Must have payments enabled
# ...
)
```
**Rules:**
- Enabling a module auto-enables its dependencies
- Disabling a module auto-disables modules that depend on it
- Core modules cannot depend on optional modules
## Services Pattern
```python
# app/modules/mymodule/services/mymodule_service.py
from sqlalchemy.orm import Session
class MyModuleService:
def get_data(self, db: Session, store_id: int):
# Business logic here
pass
# Singleton instance
mymodule_service = MyModuleService()
```
```python
# app/modules/mymodule/services/__init__.py
from app.modules.mymodule.services.mymodule_service import (
mymodule_service,
MyModuleService,
)
__all__ = ["mymodule_service", "MyModuleService"]
```
## Exceptions Pattern
```python
# app/modules/mymodule/exceptions.py
from app.exceptions import WizamartException
class MyModuleException(WizamartException):
"""Base exception for mymodule."""
pass
class MyModuleNotFoundError(MyModuleException):
"""Resource not found."""
def __init__(self, resource_id: int):
super().__init__(f"Resource {resource_id} not found")
```
## Configuration Pattern
Modules can have their own environment-based configuration using Pydantic Settings.
```python
# app/modules/mymodule/config.py
from pydantic import Field
from pydantic_settings import BaseSettings
class MyModuleConfig(BaseSettings):
"""Configuration for mymodule."""
# Settings are loaded from environment with MYMODULE_ prefix
api_timeout: int = Field(default=30, description="API timeout in seconds")
max_retries: int = Field(default=3, description="Maximum retry attempts")
batch_size: int = Field(default=100, description="Batch processing size")
enable_feature_x: bool = Field(default=False, description="Enable feature X")
model_config = {"env_prefix": "MYMODULE_"}
# Export for auto-discovery
config_class = MyModuleConfig
config = MyModuleConfig()
```
Access configuration in your code:
```python
# In services or routes
from app.modules.config import get_module_config
mymodule_config = get_module_config("mymodule")
timeout = mymodule_config.api_timeout
```
Or import directly:
```python
from app.modules.mymodule.config import config
timeout = config.api_timeout
```
Environment variables:
```bash
# .env
MYMODULE_API_TIMEOUT=60
MYMODULE_MAX_RETRIES=5
MYMODULE_ENABLE_FEATURE_X=true
```
## Background Tasks
```python
# app/modules/mymodule/tasks/__init__.py
from app.modules.mymodule.tasks.background import process_data
__all__ = ["process_data"]
```
```python
# app/modules/mymodule/tasks/background.py
from app.modules.task_base import DatabaseTask
from celery_config import celery_app
@celery_app.task(base=DatabaseTask, bind=True)
def process_data(self, data_id: int):
"""Process data in background."""
db = self.get_db()
# Use db session
pass
```
## Migrations
Modules can have their own migrations that are auto-discovered by Alembic.
### Directory Structure
```
app/modules/mymodule/migrations/
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
├── mymodule_001_initial.py
└── mymodule_002_add_feature.py
```
### Naming Convention
```
{module_code}_{sequence}_{description}.py
```
Example: `mymodule_001_create_tables.py`
### Creating the Migrations Directory
```bash
# Create migrations directory structure
mkdir -p app/modules/mymodule/migrations/versions
# Create required __init__.py files
echo '"""Module migrations."""' > app/modules/mymodule/migrations/__init__.py
echo '"""Migration versions."""' > app/modules/mymodule/migrations/versions/__init__.py
```
### Migration Template
```python
# app/modules/mymodule/migrations/versions/mymodule_001_create_tables.py
"""Create mymodule tables.
Revision ID: mymodule_001
Create Date: 2026-01-28
"""
from alembic import op
import sqlalchemy as sa
revision = "mymodule_001"
down_revision = None
branch_labels = ("mymodule",)
def upgrade() -> None:
op.create_table(
"mymodule_items",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id")),
sa.Column("name", sa.String(200), nullable=False),
)
def downgrade() -> None:
op.drop_table("mymodule_items")
```
### Running Module Migrations
Module migrations are automatically discovered. Run all migrations:
```bash
alembic upgrade head
```
The `alembic/env.py` automatically discovers module migrations via `app/modules/migrations.py`.
### Note on Existing Modules
Existing modules (CMS, billing, marketplace, etc.) currently have their migrations in the central `alembic/versions/` directory. Moving these to module-specific directories is an optional migration task that can be done incrementally.
**New modules should create their own `migrations/versions/` directory.**
## Validation
Run the architecture validator to check your module:
```bash
python scripts/validate/validate_architecture.py
```
### Validation Rules
| Rule | What It Checks |
|------|----------------|
| MOD-001 | Required directories exist |
| MOD-002 | Services contain actual code |
| MOD-003 | Schemas contain actual code |
| MOD-004 | Routes import from module |
| MOD-005 | Templates and static exist |
| MOD-006 | Locales directory exists |
| MOD-007 | Definition paths are valid |
| MOD-008 | exceptions.py exists |
| MOD-009 | definition.py exists |
| MOD-010 | Routes export `router` |
| MOD-011 | Tasks has `__init__.py` |
| MOD-012 | All locale files exist |
| MOD-013 | config.py exports config |
| MOD-014 | Migrations follow naming convention |
| MOD-015 | Migrations have `__init__.py` |
## Checklist
### New Module Checklist
- [ ] Create module directory with structure
- [ ] Create `definition.py` with ModuleDefinition
- [ ] Set `is_self_contained=True`
- [ ] Create `__init__.py` in all directories
- [ ] Create `exceptions.py`
- [ ] Create routes with `router` variable
- [ ] Create templates with namespace prefix
- [ ] Create locales (en, de, fr, lu)
- [ ] Create `config.py` if module needs environment settings (optional)
- [ ] Create `migrations/versions/` with `__init__.py` files if module has database tables
- [ ] Run `python scripts/validate/validate_architecture.py`
- [ ] Test routes are accessible
### No Framework Changes Needed
You do NOT need to:
- Edit `main.py`
- Edit `registry.py`
- Edit any configuration files
- Register routes manually
- Import the module anywhere
The framework discovers everything automatically!
## Related Documentation
- [Module System](../architecture/module-system.md) - Architecture overview
- [Menu Management](../architecture/menu-management.md) - Sidebar integration
- [Architecture Rules](architecture-rules.md) - Validation rules