Plugin System
Pagoda’s plugin system enables extension through completely independent external plugins using a 3-layer architecture. This system separates plugins from core functionality and provides stable extension points.
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Core Layer │ │ Plugin Layer │ │Extended App Layer│
│ │ │ │ │ │
│pagoda-plugin-sdk│◄───│ External Plugin │◄───│ Pagoda │
│ │ │ │ │ │
│ • Interfaces │ │ • Plugin Logic │ │ • Bridge Impl. │
│ • Base Classes │ │ • API Endpoints │ │ • URL Integration│
│ • Common Hooks │ │ • Hook Handlers │ │ • Django Setup │
└─────────────────┘ └──────────────────┘ └──────────────────┘
Plugins enable the following extensions:
- API v2 Endpoints: RESTful API extensions
- Hook-Based Extensions: Intervention and extension of core operations
- Custom Business Logic: Unique processing and data manipulation
- Authentication & Authorization Integration: Utilizing Pagoda’s permission system
Foundation layer provided as an independent PyPI package:
# pagoda_plugin_sdk provides:
from pagoda_plugin_sdk import Plugin, PluginAPIViewMixin
from pagoda_plugin_sdk.models import Entity, Entry, User
from pagoda_plugin_sdk.protocols import EntityProtocol, EntryProtocol, UserProtocol
Features:
- Distributable via PyPI
- Depends on Django/DRF but not on Pagoda application
- Type-safe Protocol definitions for host models
- Model injection mechanism for accessing host application data
Completely independent plugin that depends only on pagoda-plugin-sdk:
from pagoda_plugin_sdk import Plugin
from pagoda_plugin_sdk.decorators import entry_hook
class MyPlugin(Plugin):
id = "my-plugin"
name = "My Plugin"
version = "1.0.0"
django_apps = ["my_plugin"]
api_v2_patterns = "my_plugin.api_v2.urls"
@entry_hook("after_create")
def log_entry_create(self, entity_name, user, entry, **kwargs):
"""Called after an entry is created"""
logger.info(f"Entry created: {entry.name}")
Injects concrete model implementations into the plugin SDK:
# Pagoda injects actual models into the plugin SDK
from airone.plugins.integration import plugin_integration
# During initialization, real models are injected
plugin_integration.initialize() # Injects Entity, Entry, User, etc.
# Plugins can then access models through the SDK
from pagoda_plugin_sdk.models import Entity, Entry, User
Plugins access host application models through a safe injection mechanism. The SDK provides Protocol-based type definitions that ensure type safety without creating implementation dependencies.
The SDK defines type-safe protocols for all major models:
from pagoda_plugin_sdk.protocols import (
EntityProtocol,
EntryProtocol,
UserProtocol,
EntityAttrProtocol,
AttributeProtocol,
AttributeValueProtocol,
)
Available Protocols:
EntityProtocol- Schema definition (Entity model)EntryProtocol- Data entry (Entry model)UserProtocol- User accountEntityAttrProtocol- Entity attribute definitionAttributeProtocol- Entry attributeAttributeValueProtocol- Attribute value
Models are injected by the host application during initialization:
# In Pagoda application startup
from airone.plugins.integration import plugin_integration
# Initialize plugin system and inject models
plugin_integration.initialize() # Automatically injects real models
The injection process:
- Pagoda application starts
- Plugin system initializes
- Real Django models are injected into
pagoda_plugin_sdk.models - Plugins can safely access models through the SDK
Basic Model Access:
from pagoda_plugin_sdk.models import Entity, Entry, User
def my_plugin_view(request):
# Type-safe Entity access
entities = Entity.objects.filter(is_active=True)
# Type-safe Entry access
entries = Entry.objects.filter(schema__name="MyEntity")
# Access with relationships
for entry in entries:
entity_name = entry.schema.name # Type-safe attribute access
creator = entry.created_user.username
Checking Model Availability:
from pagoda_plugin_sdk import models
# Check if models are initialized
if models.is_initialized():
from pagoda_plugin_sdk.models import Entity
entities = Entity.objects.all()
else:
# Handle case where plugin system is not initialized
raise RuntimeError("Plugin system not initialized")
# Get list of available models
available = models.get_available_models() # ['Entity', 'Entry', 'User', ...]
Model CRUD Operations:
from pagoda_plugin_sdk.models import Entity, Entry
# Create
entity = Entity.objects.create(
name="New Entity",
note="Created by plugin",
created_user=request.user
)
# Read
entity = Entity.objects.get(id=123)
entries = Entry.objects.filter(schema=entity, is_active=True)
# Update
entity.note = "Updated by plugin"
entity.save()
# Delete (soft delete)
entity.is_active = False
entity.save()
Using Entry-Specific Methods:
from pagoda_plugin_sdk.models import Entry
# Get entry with attributes
entry = Entry.objects.get(id=456)
# Use Entry's custom methods (defined in EntryProtocol)
attrs = entry.get_attrs() # Get all attributes as dict
entry.set_attrs(user=request.user, name="value", age=30)
# Permission checking
if entry.may_permitted(request.user, some_permission):
# Perform operation
pass
Using Protocols provides several advantages:
- IDE Autocomplete: Full IntelliSense support in modern IDEs
- Type Checking: Static type checkers (mypy) can verify correctness
- No Import Errors: No circular dependency issues
- Documentation: Protocol definitions serve as API documentation
Example with Type Hints:
from typing import List
from pagoda_plugin_sdk.models import Entity, Entry
from pagoda_plugin_sdk.protocols import EntityProtocol, EntryProtocol
def get_entries_for_entity(entity: EntityProtocol) -> List[EntryProtocol]:
"""Get all active entries for a given entity
Args:
entity: Entity to query entries for
Returns:
List of active Entry instances
"""
return Entry.objects.filter(schema=entity, is_active=True)
Always handle cases where models might not be available:
from pagoda_plugin_sdk.models import Entity
from rest_framework.response import Response
from rest_framework import status
def my_view(request):
try:
if Entity is None:
return Response(
{"error": "Entity model not available"},
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
entities = Entity.objects.filter(is_active=True)
# Process entities...
except Exception as e:
return Response(
{"error": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Install pagoda-plugin-sdk from Git repository (recommended)
pip install git+https://github.com/dmm-com/pagoda.git#subdirectory=plugin/sdk
# Or using uv (faster)
uv pip install git+https://github.com/dmm-com/pagoda.git#subdirectory=plugin/sdk
# Or from local source (for SDK development)
cd /path/to/pagoda/plugin/sdk
pip install -e .
The plugin system loads only manually specified plugins:
# Enable a single plugin
export ENABLED_PLUGINS=my-plugin
# Enable multiple plugins (comma-separated)
export ENABLED_PLUGINS=my-plugin,another-plugin
# Start with uv environment (recommended)
ENABLED_PLUGINS=my-plugin uv run python manage.py runserver
# Start with pip environment
ENABLED_PLUGINS=my-plugin python manage.py runserver
# Start after setting environment variable
export ENABLED_PLUGINS=my-plugin
python manage.py runserver
Important: Only plugins explicitly specified in ENABLED_PLUGINS are loaded. If none are specified, the plugin system is disabled.
Logs when the plugin system is operating normally:
[INFO] Initializing plugin system...
[INFO] Starting plugin discovery...
[INFO] Loaded external plugin: hello-world
[INFO] Registered plugin: hello-world-plugin v1.0.0
[INFO] Connected Entry model signals to hook system
[INFO] Pagoda bridge manager initialized successfully
[INFO] Registered 2 hooks for plugin hello-world-plugin
[INFO] Plugin discovery completed. Found 1 plugins.
[INFO] Plugin system initialized successfully
Sample plugin API endpoints:
# Authentication-free test endpoint (for verification)
curl http://localhost:8000/api/v2/plugins/hello-world-plugin/test/
# Authentication-required endpoints
curl -H "Authorization: Token YOUR_TOKEN" \
http://localhost:8000/api/v2/plugins/hello-world-plugin/hello/
curl -H "Authorization: Token YOUR_TOKEN" \
http://localhost:8000/api/v2/plugins/hello-world-plugin/greet/John/
curl -H "Authorization: Token YOUR_TOKEN" \
http://localhost:8000/api/v2/plugins/hello-world-plugin/status/
{
"message": "External Hello World Plugin is working via pagoda-core!",
"plugin": {
"id": "hello-world-plugin",
"name": "Hello World Plugin",
"version": "1.0.0",
"type": "external",
"core": "pagoda-core"
},
"test": "no-auth",
"user": {
"username": "anonymous",
"is_authenticated": false
},
"pagoda_core_version": "1.0.0",
"timestamp": "2025-09-15T02:09:45.658756"
}
mkdir my-pagoda-plugin
cd my-pagoda-plugin
# Copy from example
cp -r ../plugin/examples/pagoda-hello-world-plugin/* .
my-pagoda-plugin/
├── pyproject.toml # Modern package configuration
├── Makefile # Development commands
├── README.md # Plugin documentation
└── my_plugin_package/ # Main package
├── __init__.py
├── plugin.py # Plugin class definition
├── hooks.py # Hook handlers
├── apps.py # Django app configuration
└── api_v2/ # API endpoints
├── __init__.py
├── urls.py # URL configuration
└── views.py # API view implementation
[project]
name = "my-pagoda-plugin"
version = "1.0.0"
description = "My Pagoda Plugin"
dependencies = [
"pagoda-plugin-sdk @ git+https://github.com/dmm-com/pagoda.git#subdirectory=plugin/sdk",
"Django>=3.2",
"djangorestframework>=3.12",
]
requires-python = ">=3.8"
[project.entry-points."pagoda.plugins"]
my-plugin = "my_plugin_package.plugin:MyPlugin"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
from pagoda_plugin_sdk import Plugin
from pagoda_plugin_sdk.decorators import entry_hook
import logging
logger = logging.getLogger(__name__)
class MyPlugin(Plugin):
# Required metadata
id = "my-plugin"
name = "My Plugin"
version = "1.0.0"
description = "My custom Pagoda plugin"
author = "Your Name"
# Django integration
django_apps = ["my_plugin_package"]
api_v2_patterns = "my_plugin_package.api_v2.urls"
# Hook handlers using decorators
@entry_hook("after_create")
def log_after_create(self, entity_name, user, entry, **kwargs):
"""Called after an entry is created"""
logger.info(f"Entry created: {entry.name} in {entity_name}")
@entry_hook("before_update")
def log_before_update(self, entity_name, user, validated_data, entry, **kwargs):
"""Called before an entry is updated"""
logger.info(f"Entry updating: {entry.name}")
return validated_data
from datetime import datetime
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from pagoda_plugin_sdk import PluginAPIViewMixin
class MyAPIView(PluginAPIViewMixin):
permission_classes = [AllowAny] # For testing
def get(self, request):
return Response({
"message": "Hello from my plugin!",
"plugin": {
"id": self.plugin_id,
"name": "My Plugin",
"version": "1.0.0"
},
"timestamp": datetime.now().isoformat()
})
Development Make commands in plugin directory:
make help # Show available commands
make dev-setup # Set up development environment
make install-dev # Install in development mode
make test # Run plugin tests
make test-integration # Run Pagoda integration tests
make build # Build distribution packages
make publish-test # Publish to TestPyPI
make publish # Publish to PyPI
# Build and publish
make build
make publish
# Users install the plugin
pip install my-pagoda-plugin
# Create tag and release
git tag v1.0.0
git push origin v1.0.0
# Users install from GitHub release
pip install https://github.com/user/my-plugin/releases/download/v1.0.0/my_plugin-1.0.0-py3-none-any.whl
# Development editable install
pip install -e .
# Development install in Poetry environment
pip install -e .
A complete sample plugin is available at plugin/examples/pagoda-hello-world-plugin/:
Endpoints:
GET /api/v2/plugins/hello-world-plugin/test/- Authentication-free testGET /api/v2/plugins/hello-world-plugin/hello/- Basic Hello APIPOST /api/v2/plugins/hello-world-plugin/hello/- Custom message APIGET /api/v2/plugins/hello-world-plugin/greet/<name>/- Personalized greetingGET /api/v2/plugins/hello-world-plugin/status/- Plugin statusGET /api/v2/plugins/hello-world-plugin/entities/- List all entities (demonstrates model access)GET /api/v2/plugins/hello-world-plugin/entities/<id>/- Get entity detailsGET /api/v2/plugins/hello-world-plugin/entries/- List entries with filteringGET /api/v2/plugins/hello-world-plugin/entries/<id>/- Get entry with attributes
Model Access Examples:
The sample plugin demonstrates how to access host application models:
from pagoda_plugin_sdk import PluginAPIViewMixin
from pagoda_plugin_sdk.models import Entity, Entry
class EntityListView(PluginAPIViewMixin):
def get(self, request):
# Type-safe entity access
entities = Entity.objects.filter(is_active=True)
entity_list = [
{
"id": entity.id,
"name": entity.name,
"note": entity.note,
}
for entity in entities
]
return Response({"entities": entity_list})
| Variable | Default | Description |
|---|---|---|
ENABLED_PLUGINS | [] | List of plugins to enable (comma-separated) |
# airone/settings_common.py
ENABLED_PLUGINS = env.list("ENABLED_PLUGINS", default=[])
# Plugin system is automatically enabled if any plugins are specified
PLUGINS_ENABLED = bool(ENABLED_PLUGINS)
# Plugin apps are dynamically added to INSTALLED_APPS
if PLUGINS_ENABLED:
INSTALLED_APPS.extend(plugin_integration.get_installed_apps())
The plugin system adopts explicit control:
- No Automatic Discovery: Does not automatically load available plugins
- Explicit Specification: Only loads plugins specified in
ENABLED_PLUGINS - Security: Prevents loading of unintended plugins
- Controllability: Easy plugin control in development and production environments
Symptoms: curl http://localhost:8000/api/v2/plugins/my-plugin/test/ returns 404
Causes and Solutions:
# Cause 1: Plugin not specified
❌ python manage.py runserver
✅ ENABLED_PLUGINS=my-plugin python manage.py runserver
# Cause 2: Plugin not installed
❌ pip install -e plugin_examples/my-plugin/
✅ cd plugin_examples/my-plugin/ && pip install -e .
# Cause 3: Incorrect Entry points path
❌ 'my-plugin = my_plugin:MyPlugin'
✅ 'my-plugin = my_plugin.plugin:MyPlugin'
# Cause 4: Incorrect Entry points group name
❌ [project.entry-points."airone.plugins"]
✅ [project.entry-points."pagoda.plugins"]
Log Example:
[ERROR] Failed to load external plugin my-plugin: No module named 'my_plugin'
[INFO] Plugin discovery completed. Found 0 plugins.
Resolution Steps:
- Check Entry points:
python -c "
import pkg_resources
for ep in pkg_resources.iter_entry_points('pagoda.plugins'):
print(f'{ep.name} -> {ep.module_name}:{ep.attrs[0]}')
try:
plugin_class = ep.load()
print(f'✓ Load successful: {plugin_class}')
except Exception as e:
print(f'✗ Load failed: {e}')
"
- Reinstall Plugin:
cd my-plugin/
pip uninstall -y my-plugin
rm -rf build/ dist/ *.egg-info/
pip install -e .
- Check Path and Module:
python -c "
import sys
sys.path.insert(0, 'plugin_examples/my-plugin')
from my_plugin.plugin import MyPlugin
print(f'✓ Direct import works: {MyPlugin().name}')
"
Log Example:
[ERROR] Hook entry.after_create failed: missing required arguments
Solution: Implement correct hook handler signature with decorator
# ❌ Wrong (missing decorator)
def log_after_create(self, entity_name, user, entry, **kwargs):
pass
# ❌ Wrong (incorrect signature)
@entry_hook("after_create")
def log_after_create(user, entry):
pass
# ✅ Correct
@entry_hook("after_create")
def log_after_create(self, entity_name, user, entry, **kwargs):
"""
entity_name: Name of the entity
user: User who created the entry
entry: The created Entry instance
**kwargs: Additional context
"""
logger.info(f"New entry created: {entry.name} in {entity_name}")
Development with Poetry:
# If pagoda-core is not found
cd pagoda-core/
make install-dev
# Install plugin for development
cd ../plugin_examples/my-plugin/
pip install -e .
# Run integration tests
make test-integration
# Check plugin status
ENABLED_PLUGINS=hello-world python manage.py shell -c "
from airone.plugins.integration import plugin_integration
plugin_integration.initialize()
print(f'Plugins: {plugin_integration.get_enabled_plugin_count()}')
for plugin in plugin_integration.get_enabled_plugins():
print(f' - {plugin.name} ({plugin.id}) v{plugin.version}')
"
# Test URL resolution
python -c "
import django
django.setup()
from django.urls import get_resolver
resolver = get_resolver()
match = resolver.resolve('/api/v2/plugins/hello-world-plugin/test/')
print(f'✓ URL resolved: {match.func}')
"
# Get hook statistics
python -c "
from airone.plugins.bridge_manager import bridge_manager
bridge_manager.initialize()
stats = bridge_manager.hooks.get_hook_statistics()
print(f'Total hooks: {stats[\"total_hooks\"]}')
print(f'Registered hooks: {stats[\"registered_hooks\"]}')
"
1. External Plugin Discovery (Entry Points)
├─ pkg_resources.iter_entry_points('pagoda.plugins')
├─ Load plugin class from entry point
└─ Register with plugin_registry
2. Example Plugin Discovery (Directory Scan)
├─ Scan plugin_examples/ directory
├─ Import plugin.py from each plugin directory
└─ Register discovered plugin classes
3. Plugin Integration
├─ Django Apps → INSTALLED_APPS integration
├─ URL Patterns → api_v2/urls.py integration
├─ Hook Registration → bridge_manager.hooks
└─ Bridge System Initialization
The plugin system provides a comprehensive hook system with 17 standard hooks organized into four categories:
1. Entry Lifecycle Hooks
entry.before_create- Called before an entry is createdentry.after_create- Called after an entry is createdentry.before_update- Called before an entry is updatedentry.after_update- Called after an entry is updatedentry.before_delete- Called before an entry is deletedentry.before_restore- Called before an entry is restoredentry.after_restore- Called after an entry is restored
2. Entity Lifecycle Hooks
entity.before_create- Called before an entity is createdentity.after_create- Called after an entity is createdentity.before_update- Called before an entity is updatedentity.after_update- Called after an entity is updated
3. Validation Hooks
entry.validate- Custom validation for entry creation/update
4. Data Access Hooks
entry.get_attrs- Modify entry attributes before returning to cliententity.get_attrs- Modify entity attributes before returning to client
The plugin SDK provides four decorators for registering hook handlers:
1. @entry_hook(hook_name, entity=None, priority=100)
For Entry lifecycle hooks. Supports entity-specific filtering.
from pagoda_plugin_sdk.decorators import entry_hook
class MyPlugin(Plugin):
# Apply to all entities
@entry_hook("after_create")
def log_all_entries(self, entity_name, user, entry, **kwargs):
logger.info(f"Entry created: {entry.name}")
# Apply only to specific entity
@entry_hook("after_create", entity="customer")
def log_customer_only(self, entity_name, user, entry, **kwargs):
logger.info(f"Customer entry created: {entry.name}")
2. @entity_hook(hook_name, priority=100)
For Entity lifecycle hooks.
from pagoda_plugin_sdk.decorators import entity_hook
class MyPlugin(Plugin):
@entity_hook("after_create")
def log_entity_create(self, user, entity, **kwargs):
logger.info(f"Entity created: {entity.name}")
3. @validation_hook(priority=100)
For entry validation (entry.validate hook).
from pagoda_plugin_sdk.decorators import validation_hook
class MyPlugin(Plugin):
@validation_hook()
def validate_entry(self, user, schema_name, name, attrs, instance, **kwargs):
if "forbidden" in name.lower():
raise ValueError("Name cannot contain 'forbidden'")
4. @get_attrs_hook(target, priority=100)
For data access hooks. Target must be either “entry” or “entity”.
from pagoda_plugin_sdk.decorators import get_attrs_hook
class MyPlugin(Plugin):
@get_attrs_hook("entry")
def modify_entry_attrs(self, entry, attrinfo, is_retrieve, **kwargs):
# Add custom field
for attr in attrinfo:
attr["custom_flag"] = True
return attrinfo
@get_attrs_hook("entity")
def modify_entity_attrs(self, entity, attrinfo, **kwargs):
return attrinfo
Entry hooks support entity-specific filtering using the entity parameter:
class MyPlugin(Plugin):
# Runs only for "product" entity
@entry_hook("after_create", entity="product")
def handle_product_create(self, entity_name, user, entry, **kwargs):
# Only called when a product entry is created
pass
# Runs for all entities
@entry_hook("after_create")
def handle_any_create(self, entity_name, user, entry, **kwargs):
# Called for all entry creations
pass
When both entity-specific and generic hooks are registered, both will be executed in priority order.
Hooks are executed in priority order (lower number = higher priority, default is 100):
class MyPlugin(Plugin):
# Runs first (priority 50)
@entry_hook("after_create", priority=50)
def first_handler(self, entity_name, user, entry, **kwargs):
logger.info("Runs first")
# Runs second (default priority 100)
@entry_hook("after_create")
def second_handler(self, entity_name, user, entry, **kwargs):
logger.info("Runs second")
# Runs last (priority 150)
@entry_hook("after_create", priority=150)
def third_handler(self, entity_name, user, entry, **kwargs):
logger.info("Runs last")
This is useful for ensuring proper execution order when multiple plugins handle the same hook.
Each hook type has a specific signature:
Entry Lifecycle Hooks:
def handler(self, entity_name: str, user: User, entry: Entry, **kwargs) -> None:
# For after_create, after_update, after_delete, after_restore
pass
def handler(self, entity_name: str, user: User, validated_data: dict, **kwargs) -> dict:
# For before_create - can modify data
return validated_data
def handler(self, entity_name: str, user: User, validated_data: dict, entry: Entry, **kwargs) -> dict:
# For before_update - can modify data
return validated_data
def handler(self, entity_name: str, user: User, entry: Entry, **kwargs) -> None:
# For before_delete, before_restore
pass
Entity Lifecycle Hooks:
def handler(self, user: User, entity: Entity, **kwargs) -> None:
# For after_create, after_update
pass
def handler(self, user: User, validated_data: dict, **kwargs) -> dict:
# For before_create - can modify data
return validated_data
def handler(self, user: User, validated_data: dict, entity: Entity, **kwargs) -> dict:
# For before_update - can modify data
return validated_data
Validation Hook:
def handler(self, user: User, schema_name: str, name: str,
attrs: list, instance: Optional[Entry], **kwargs) -> None:
# Raise ValueError or ValidationError to reject
if invalid_condition:
raise ValueError("Validation error message")
Data Access Hooks:
def handler(self, entry: Entry, attrinfo: list, is_retrieve: bool, **kwargs) -> list:
# For entry.get_attrs - must return modified attrinfo
return attrinfo
def handler(self, entity: Entity, attrinfo: list, **kwargs) -> list:
# For entity.get_attrs - must return modified attrinfo
return attrinfo
The hook system maintains backward compatibility with the legacy custom_view system through hook name aliases:
# Legacy custom_view hook names are automatically mapped to standard names
HOOK_ALIASES = {
"before_create_entry_v2": "entry.before_create",
"after_create_entry_v2": "entry.after_create",
"before_update_entry_v2": "entry.before_update",
"after_update_entry_v2": "entry.after_update",
"before_delete_entry_v2": "entry.before_delete",
"validate_entry": "entry.validate",
"get_entry_attr": "entry.get_attrs",
"get_entity_attr": "entity.get_attrs",
# ... and more
}
This allows existing custom_view implementations to work with the new plugin system without modification.
# Pagoda Hook Manager implementation
class HookManager:
def execute_hook(self, hook_name, *args, entity_name=None, **kwargs):
# 1. Normalize hook name (handle aliases)
# 2. Get all registered handlers
# 3. Filter by entity if specified
# 4. Sort by priority
# 5. Execute each handler with error isolation
# 6. Collect and return results
Key features:
- Error Isolation: One plugin’s failure doesn’t affect others
- Priority Ordering: Handlers execute in priority order
- Entity Filtering: Entity-specific hooks run only for matching entities
- Flexible Signatures: Different hook types have different signatures
The host application (Pagoda) injects concrete model implementations into the plugin SDK during initialization. This allows plugins to access Pagoda’s data models without creating direct dependencies.
The following six models are automatically injected:
- Entity - Schema definition (from
entity.models.Entity) - EntityAttr - Entity attribute definition (from
entity.models.EntityAttr) - Entry - Data entry (from
entry.models.Entry) - Attribute - Entry attribute (from
entry.models.Attribute) - AttributeValue - Attribute value (from
entry.models.AttributeValue) - User - User account (from
user.models.User)
# airone/plugins/integration.py
class PluginIntegration:
def _inject_models(self):
"""Inject real models into the plugin SDK"""
try:
# Import real models from Pagoda
import pagoda_plugin_sdk.models as sdk_models
from entity.models import Entity, EntityAttr
from entry.models import Entry, Attribute, AttributeValue
from user.models import User
# Inject real models into SDK namespace
sdk_models.Entity = Entity
sdk_models.Entry = Entry
sdk_models.User = User
sdk_models.AttributeValue = AttributeValue
sdk_models.EntityAttr = EntityAttr
sdk_models.Attribute = Attribute
logger.info("Successfully injected models into plugin SDK")
except ImportError as e:
logger.error(f"Failed to inject models into plugin SDK: {e}")
raise
1. Pagoda Application Startup
└─ settings_common.py loads ENABLED_PLUGINS from environment
2. Plugin System Initialization
├─ PluginIntegration.initialize() is called
├─ discover_plugins() finds and registers plugins
└─ _inject_models() injects real models into SDK
3. Plugin Access
└─ Plugins can now safely import and use models
# In plugin code
from pagoda_plugin_sdk.models import Entity, Entry, User
class MyPlugin(Plugin):
@entry_hook("after_create")
def handle_entry_create(self, entity_name, user, entry, **kwargs):
# Direct access to model methods
entity = entry.schema # Access related Entity
creator = entry.created_user # Access related User
attrs = entry.get_attrs() # Call Entry methods
# Query operations
all_entries = Entry.objects.filter(schema=entity)
active_users = User.objects.filter(is_active=True)
The SDK provides Protocol definitions for type checking without creating implementation dependencies:
from pagoda_plugin_sdk.protocols import (
EntityProtocol,
EntryProtocol,
UserProtocol,
EntityAttrProtocol,
AttributeProtocol,
AttributeValueProtocol,
)
def process_entry(entry: EntryProtocol) -> dict:
"""Type-safe entry processing with IDE support"""
return {
"id": entry.id,
"name": entry.name,
"schema": entry.schema.name, # Full IntelliSense support
}
Always check if models are available before using them:
from pagoda_plugin_sdk import models
from pagoda_plugin_sdk.models import Entity
def safe_operation():
# Check if plugin system is initialized
if not models.is_initialized():
raise RuntimeError("Plugin system not initialized")
# Check specific model
if Entity is None:
raise RuntimeError("Entity model not available")
# Safe to proceed
entities = Entity.objects.all()
This injection mechanism ensures that plugins remain independent of Pagoda’s implementation while still having full access to its data models.
- Version Pinning: Ensure compatibility with
pagoda-core>=1.0.0,<2.0.0 - Testing: Implement both unit tests and Pagoda integration tests
- Documentation: Include README and API specifications
- Error Handling: Implement proper exception handling in hooks and APIs
- Security: Implement proper authentication and authorization
- Semantic Versioning: Use appropriate
major.minor.patchversioning - Changelog: Maintain release notes and change history
- Compatibility: Clearly specify supported Pagoda/pagoda-plugin-sdk versions
- Dependencies: Keep dependencies to a minimum
- Environment Isolation: Isolate virtual environments per plugin
- Monitoring: Monitor plugin error logs
- Rollback Strategy: Prepare procedures for disabling plugins
- Performance: Evaluate performance impact of hook processing
This 3-layer architecture realizes a plugin system completely independent from Pagoda. Plugin developers can create safe and reusable plugins depending only on pagoda-plugin-sdk.