Add self-contained configuration and migrations support for modules:
Config auto-discovery (app/modules/config.py):
- Modules can have config.py with Pydantic Settings
- Environment variables prefixed with MODULE_NAME_
- Auto-discovered via get_module_config()
Migrations auto-discovery:
- Each module has migrations/versions/ directory
- Alembic discovers module migrations automatically
- Naming convention: {module}_{seq}_{description}.py
New architecture rules (MOD-013 to MOD-015):
- MOD-013: config.py should export config/config_class
- MOD-014: Migrations must follow naming convention
- MOD-015: Migrations directory must have __init__.py
Created for all 11 self-contained modules:
- config.py placeholder files
- migrations/ directories with __init__.py files
Added core and tenancy module definitions for completeness.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
560 lines
15 KiB
Markdown
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/vendor,static/vendor/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.VENDOR: ["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/vendor.py
|
|
from fastapi import APIRouter, Depends
|
|
from app.api.deps import get_current_vendor_api, get_db
|
|
|
|
router = APIRouter() # MUST be named 'router'
|
|
|
|
@router.get("")
|
|
def get_mymodule_data(current_user=Depends(get_current_vendor_api)):
|
|
return {"message": "Hello from mymodule"}
|
|
```
|
|
|
|
```python
|
|
# app/modules/mymodule/routes/pages/vendor.py
|
|
from fastapi import APIRouter, Request, Path
|
|
from fastapi.responses import HTMLResponse
|
|
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db, Depends
|
|
from app.templates_config import templates
|
|
|
|
router = APIRouter() # MUST be named 'router'
|
|
|
|
@router.get("/{vendor_code}/mymodule", response_class=HTMLResponse)
|
|
async def mymodule_page(
|
|
request: Request,
|
|
vendor_code: str = Path(...),
|
|
current_user=Depends(get_current_vendor_from_cookie_or_header),
|
|
db=Depends(get_db),
|
|
):
|
|
return templates.TemplateResponse(
|
|
"mymodule/vendor/index.html",
|
|
{"request": request, "vendor_code": vendor_code},
|
|
)
|
|
```
|
|
|
|
### Step 4: Done!
|
|
|
|
That's it! The framework automatically:
|
|
- Discovers and registers the module
|
|
- Mounts API routes at `/api/v1/vendor/mymodule`
|
|
- Mounts page routes at `/vendor/{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
|
|
│ │ └── vendor.py # /api/v1/vendor/mymodule
|
|
│ └── pages/
|
|
│ ├── __init__.py
|
|
│ ├── admin.py # /admin/mymodule
|
|
│ └── vendor.py # /vendor/{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
|
|
│ └── vendor/
|
|
│ └── index.html
|
|
│
|
|
├── static/ # Static files (auto-mounted)
|
|
│ ├── admin/
|
|
│ │ └── js/
|
|
│ │ └── mymodule.js
|
|
│ └── vendor/
|
|
│ └── 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
|
|
└── vendor/
|
|
└── index.html
|
|
```
|
|
|
|
Reference in code:
|
|
```python
|
|
templates.TemplateResponse("mymodule/vendor/index.html", {...})
|
|
```
|
|
|
|
### Static File URLs
|
|
|
|
Static files are mounted at `/static/modules/{module_code}/`:
|
|
|
|
```html
|
|
<!-- In template -->
|
|
<script src="{{ url_for('mymodule_static', path='vendor/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 vendors.
|
|
|
|
```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, vendor_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("vendor_id", sa.Integer(), sa.ForeignKey("vendors.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_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_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
|