Create a Plugin¶
This guide walks you through building an AMSDAL plugin — from defining configuration to adding routes, middleware, models, and event listeners.
Plugin Structure¶
A typical plugin has this layout:
my_plugin/
├── __init__.py
├── app.py # AppConfig class
├── models/ # Plugin models
│ ├── __init__.py
│ └── my_model.py
├── transactions/ # Plugin transactions
│ ├── __init__.py
│ └── my_transactions.py
├── event_handlers/ # Event listeners
│ ├── __init__.py
│ └── listeners.py
└── fixtures/ # Default data (optional)
└── initial.json
Step 1: Define AppConfig¶
Every plugin needs an AppConfig subclass. The on_setup() method is called when the plugin is loaded.
# my_plugin/app.py
from amsdal.contrib.app_config import AppConfig
class MyPluginAppConfig(AppConfig):
def on_setup(self) -> None:
# Register event listeners, configure the plugin
from amsdal_utils.events import EventBus
from amsdal_server.apps.common.events.server import (
RouterSetupEvent,
ServerStartupEvent,
)
from my_plugin.event_handlers.listeners import (
MyRouteListener,
MyStartupListener,
)
EventBus.subscribe(RouterSetupEvent, MyRouteListener)
EventBus.subscribe(ServerStartupEvent, MyStartupListener)
Step 2: Register the Plugin¶
Add your AppConfig class path to the AMSDAL_CONTRIBS setting:
# Environment variable (comma-separated)
AMSDAL_CONTRIBS="amsdal.contrib.auth.app.AuthAppConfig,amsdal.contrib.frontend_configs.app.FrontendConfigAppConfig,my_plugin.app.MyPluginAppConfig"
AMSDAL will import and call on_setup() for each config during application initialization.
Step 3: Add Event Listeners¶
Use the Events System to hook into the AMSDAL lifecycle.
# my_plugin/event_handlers/listeners.py
from amsdal_utils.events import EventListener, NextFn, AsyncNextFn
from amsdal_server.apps.common.events.server import (
ServerStartupContext,
)
class MyStartupListener(EventListener[ServerStartupContext]):
def handle(self, context: ServerStartupContext, next_fn: NextFn):
print('Plugin initialized!')
return next_fn(context)
async def ahandle(self, context: ServerStartupContext, next_fn: AsyncNextFn):
print('Plugin initialized (async)!')
return await next_fn(context)
Step 4: Add Custom Routes¶
Listen to RouterSetupEvent to add API endpoints via FastAPI routers:
from fastapi import APIRouter
from amsdal_utils.events import EventListener, NextFn
from amsdal_server.apps.common.events.server import (
RouterSetupContext,
)
class MyRouteListener(EventListener[RouterSetupContext]):
def handle(self, context: RouterSetupContext, next_fn: NextFn):
router = APIRouter(tags=['my-plugin'])
@router.get('/api/my-plugin/status')
async def plugin_status():
return {'status': 'active'}
@router.post('/api/my-plugin/action')
async def plugin_action(data: dict):
# Handle the action
return {'result': 'ok'}
context.app.include_router(router)
return next_fn(context)
async def ahandle(self, context, next_fn):
return self.handle(context, next_fn)
Note
RouterSetupEvent is emitted synchronously. Implement the handle() method.
Step 5: Add Custom Middleware¶
Listen to MiddlewareSetupEvent to add middleware:
from starlette.middleware.base import BaseHTTPMiddleware
from amsdal_utils.events import EventListener, NextFn
from amsdal_server.apps.common.events.server import (
MiddlewareSetupContext,
)
class RequestTimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
import time
start = time.time()
response = await call_next(request)
response.headers['X-Process-Time'] = str(time.time() - start)
return response
class MyMiddlewareListener(EventListener[MiddlewareSetupContext]):
def handle(self, context: MiddlewareSetupContext, next_fn: NextFn):
context.app.add_middleware(RequestTimingMiddleware)
return next_fn(context)
async def ahandle(self, context, next_fn):
return self.handle(context, next_fn)
Step 6: Add Models¶
Place model files in a models/ directory within your plugin package. AMSDAL automatically discovers them.
# my_plugin/models/audit_log.py
from amsdal_models.classes.model import Model
from amsdal_utils.models.enums import ModuleType
class AuditLog(Model):
__module_type__ = ModuleType.CONTRIB
action: str
user_email: str
details: str
timestamp: str
Mark contrib models with __module_type__ = ModuleType.CONTRIB so AMSDAL recognizes them as plugin-provided models and handles their migrations.
Step 7: Add Transactions¶
Place transaction files in a transactions/ directory within your plugin package. AMSDAL automatically discovers them via AST parsing and exposes them through the REST API.
# my_plugin/transactions/audit.py
from amsdal_data.transactions.decorators import transaction
from amsdal.contrib.auth.decorators import require_auth
@require_auth
@transaction(tags=['Audit'])
def export_audit_log(date_from: str, date_to: str) -> dict:
"""Export audit log entries for a date range."""
from my_plugin.models.audit_log import AuditLog
entries = list(
AuditLog.objects.filter(
timestamp__gte=date_from,
timestamp__lte=date_to,
)
)
return {'count': len(entries), 'entries': [e.to_dict() for e in entries]}
Once discovered, the transaction is available via REST API:
GET /api/transactions/— lists all transactions including plugin onesGET /api/transactions/export_audit_log/— schema and parameter detailsPOST /api/transactions/export_audit_log/— execute with{"args": {"date_from": "2025-01-01", "date_to": "2025-12-31"}}
Use @async_transaction() for async transactions. Auth decorators (@require_auth, @allow_any, @permissions()) must be placed above the transaction decorator.
Note
No explicit registration is needed — AMSDAL scans the transactions/ directory of every registered contrib automatically.
Step 8: Modify Responses (Optional)¶
Use pre-response events to enrich API responses:
from amsdal_utils.events import EventBus
from amsdal_server.apps.objects.events.pre_response import (
ObjectListPreResponseEvent,
ObjectListPreResponseContext,
)
class EnrichListResponseListener(EventListener[ObjectListPreResponseContext]):
async def ahandle(self, context: ObjectListPreResponseContext, next_fn):
# Add custom data to the response before it's sent
return await next_fn(context)
def handle(self, context, next_fn):
raise NotImplementedError
Register in on_setup():
EventBus.subscribe(ObjectListPreResponseEvent, EnrichListResponseListener)
Complete Example¶
Here's a minimal but complete plugin:
# my_analytics/app.py
from amsdal.contrib.app_config import AppConfig
class AnalyticsAppConfig(AppConfig):
def on_setup(self) -> None:
from amsdal_utils.events import EventBus
from amsdal_server.apps.common.events.server import RouterSetupEvent
from my_analytics.routes import AnalyticsRouteListener
EventBus.subscribe(RouterSetupEvent, AnalyticsRouteListener)
# my_analytics/routes.py
from fastapi import APIRouter
from amsdal_utils.events import EventListener, NextFn
from amsdal_server.apps.common.events.server import RouterSetupContext
class AnalyticsRouteListener(EventListener[RouterSetupContext]):
def handle(self, context: RouterSetupContext, next_fn: NextFn):
router = APIRouter(tags=['analytics'])
@router.get('/api/analytics/summary')
async def summary():
return {'total_users': 42, 'active_today': 10}
context.app.include_router(router)
return next_fn(context)
async def ahandle(self, context, next_fn):
return self.handle(context, next_fn)
Register:
AMSDAL_CONTRIBS="amsdal.contrib.auth.app.AuthAppConfig,amsdal.contrib.frontend_configs.app.FrontendConfigAppConfig,my_analytics.app.AnalyticsAppConfig"
Packaging & Distribution¶
To distribute your plugin as a Python package:
- Create a standard
pyproject.tomlwith your plugin package - Add
amsdalas a dependency - Document the
AMSDAL_CONTRIBSpath users need to add - Publish to PyPI
[project]
name = "amsdal-my-plugin"
dependencies = ["amsdal>=1.0"]
Users install and register:
pip install amsdal-my-plugin
AMSDAL_CONTRIBS="...,my_plugin.app.MyPluginAppConfig"