A lightweight plugin registry for Django with admin management and registry→DB sync.
Project description
Django Plugin System
A lightweight, batteries-included plugin registry for Django apps.
Use it to expose swappable implementations (e.g., multiple OTP providers) behind a stable interface, select the active plugin in Django Admin, and keep the database in sync with in‑code registrations via a management command.
✨ Features
- Simple interface-first design — define an abstract base class (ABC), register implementations.
- Registry → DB sync —
pluginsyncmanagement command (and optionalpost_migratesignal). - Admin UX — filter/search, bulk enable/disable, and quick priority nudges.
- Deterministic selection — pick a single plugin by
statusandpriority, with caching. - Safe defaults —
get_or_createsyncing preserves admin-edited fields. - Uniqueness guarantees — unique constraints for types and items.
📦 Installation
pip install django-plugin-system
Add the app to INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
# ...
"django_plugin_system",
]
The app creates two models:
PluginTypeandPluginItem.
🔌 Core Concepts
1) Define an interface (ABC)
# apps/otp/interfaces.py
from abc import ABC, abstractmethod
class AbstractOTP(ABC):
@abstractmethod
def send_otp(self, number: str, code: str) -> None: ...
2) Register a plugin type
# apps/otp/apps.py (or any module imported at startup)
from django.apps import AppConfig
from django_plugin_system.register import register_plugin_type
class OtpConfig(AppConfig):
name = "apps.otp"
def ready(self):
register_plugin_type({
"name": "otp",
"manager": self.name, # the app providing the type
"interface": AbstractOTP,
"description": "One-time password (OTP) delivery channel",
})
3) Implement and register plugin items
# apps/otp_sms/plugins.py
from django_plugin_system.register import register_plugin_item
from .interfaces import AbstractOTP
class SmsIrOTP(AbstractOTP):
def send_otp(self, number: str, code: str) -> None:
# call provider api...
pass
# Register this implementation
register_plugin_item({
"name": "sms_ir",
"module": "apps.otp_sms", # the app providing the item
"type_name": "otp",
"manager_name": "apps.otp", # the app providing the type
"plugin_class": SmsIrOTP,
"priority": 10,
"description": "Send OTP using Sms.ir provider",
})
You can register multiple items with different priorities. Lower number means higher priority.
🗄️ Database Models
# django_plugin_system.models
class PluginType(models.Model):
id = UUID PK
name = CharField
manager = CharField # the app that defines the interface
description = TextField
class PluginItem(models.Model):
id = UUID PK
plugin_type = FK -> PluginType
module = CharField # the app that provides the implementation
name = CharField
status = TextChoices('active', 'reserve', 'disable')
priority = SmallIntegerField # lower is better
description = TextField
Uniqueness
PluginType(name, manager)is unique.PluginItem(name, module, plugin_type)is unique.
🧠 Selection Logic
- Active first: pick the lowest-priority
activeitem. - Fallback: if no
active, pick the lowest-priorityreserveitem. - Cache: the chosen item is cached per
PluginTypeand auto‑invalidated on save/delete.
Helper
from django_plugin_system.helpers import get_plugin_instance
otp = get_plugin_instance("otp", "apps.otp")
if otp:
otp.send_otp("+31123456789", "123456")
Or call
PluginType.get_single_plugin()then.load_class()to instantiate manually.
🛠️ Syncing the Registry
You have two ways to keep the DB aligned with the in‑memory registry:
-
Automatically after migrations (default)
- The
post_migratesignal syncs in create-only mode (preserves admin edits).
- The
-
Manually via command
python manage.py pluginsync # or to refresh defaults from registry (overwrites description/priority on conflicts): python manage.py pluginsync --mode update # skip pruning of stale rows: python manage.py pluginsync --no-prune
Modes:
create→ usesget_or_create(safe: won’t overwritestatus/prioritychanged in Admin)update→ usesupdate_or_create(refreshdescription/priorityfrom code)
🧭 Admin Panel
- PluginType list shows counts per status.
- PluginItem list lets you:
- quick-edit
statusandpriority, - bulk mark ACTIVE/RESERVED/DISABLED,
- Increase/Decrease priority in-place,
- view a “Class loads” boolean to catch broken registrations.
- quick-edit
Changing status/priority automatically clears the single‑plugin selection cache.
🔄 Overriding selection
You can override the selection logic per type by providing a get_plugin callable in the type registry entry:
def my_selector(plugin_type_model_obj):
# your custom logic (possibly data-driven)
return plugin_type_model_obj.get_active_plugins()[0]
register_plugin_type({
"name": "otp",
"manager": "apps.otp",
"interface": AbstractOTP,
"description": "OTP delivery",
"get_plugin": my_selector, # <- override
})
🧪 Testing tips
- Ensure your registry code paths are imported in test settings (e.g., via
AppConfig.ready). - Use
pluginsync --mode createin test setup to materialize rows. - Toggle item
statusin tests to verify fallback and cache invalidation.
📐 Design Notes & Guarantees
- Registry data lives in memory at import time; DB represents a materialized view used by Admin and runtime selection.
- Syncing is idempotent and safe to run many times.
- Items are validated to implement the declared interface.
- Errors on registration are not swallowed — mis-registrations fail early and loudly.
🤔 Why Use Django Plugin System?
When you build extensible Django apps — like payment gateways, OTP senders, or notification systems — you often need pluggable backends that can be swapped or prioritized without touching your core logic.
This library lets you define interfaces, register multiple implementations, and let users (or admins) pick which ones are active — all without breaking code, and with database-level configurability.
🧩 Example: Notification System
Imagine you have multiple ways to notify users:
-
Email
-
SMS
-
Push notification
Each of these is handled by a different piece of code — maybe even from different apps.
🚫 Without Django Plugin System
- You hardcode your imports and logic:
# notifications/core.py
from notifications.email_sender import send_email
from notifications.sms_sender import send_sms
from notifications.push_sender import send_push
def notify_user(user, message):
if user.prefers_email:
send_email(user.email, message)
elif user.prefers_sms:
send_sms(user.phone, message)
elif user.prefers_push:
send_push(user.device_token, message)
- Every time you add a new provider, you must:
- Write new import statements
- Modify your logic
- Re-deploy your code
- Possibly break something that used to work
And there’s no way for an admin to change behavior dynamically — everything is baked into code.
✅ With Django Plugin System
- You define one interface:
from abc import ABC, abstractmethod
class AbstractNotifier(ABC):
@abstractmethod
def send(self, user, message): ...
- You register it as a plugin type:
from django_plugin_system.register import register_plugin_type
register_plugin_type({
"name": "notifier",
"manager": "apps.notifications",
"interface": AbstractNotifier,
"description": "Notification channels for users",
})
- You implement as many plugin items as you want (completely independent of the rest of the code):
from django_plugin_system.register import register_plugin_item
from .interfaces import AbstractNotifier
class EmailNotifier(AbstractNotifier):
def send(self, user, message):
print(f"Sending email to {user.email}: {message}")
register_plugin_item({
"name": "email",
"module": "apps.notifications.email",
"type_name": "notifier",
"manager_name": "apps.notifications",
"plugin_class": EmailNotifier,
"priority": 5,
"description": "Send notification via Email",
})
- You can now dynamically select from Admin which notifiers are active, in reserve, or disabled — even reorder them by priority.
- Your code doesn’t change at all:
from django_plugin_system.helpers import get_plugin_instance
notifier = get_plugin_instance("notifier", "apps.notifications")
notifier.send(user, "Your OTP is 1234")
💡 You can even expose multiple active notifiers and let users subscribe to their favorites — Email + Push for one user, Push only for another — all configurable through database records instead of code edits.
# models.py
from typing import List
from django.db import models
from django.contrib.auth.models import User
from django_plugin_system.models import PluginItem
class UserNotifyPref(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
favourite_plugins = models.ManyToManyField(PluginItem)
@staticmethod
def get_user_plugins(user: User) -> List[PluginItem]:
try:
return list(UserNotifyPref.objects.get(user=user).favourite_plugins.all())
except UserNotifyPref.DoesNotExist:
return []
Now the following code will notify user through all selected plugins:
from myapp.models import UserNotifyPref
...
favourite_plugins = UserNotifyPref.get_user_plugins(user)
for plugin in favourite_plugins:
plugin_service = plugin.load_class()
plugin_service.send(user, message)
...
just as simple as you see, with django-plugin-system, you can let users decide which plugin they prefer to use.
⚖️ Summary — With vs Without
| Aspect | Without Plugin System | With Plugin System |
|---|---|---|
| Adding a new provider | Requires code change + deploy | Just register plugin class |
| Selecting active provider | Hardcoded logic | Done in Django Admin |
| Prioritization / fallback | Manual if-else chain | Automatic by priority |
| Runtime swapping | Not possible | Fully supported |
| Testing new providers | Requires staging deployment | Just toggle “active/reserve” |
| Extensibility | Rigid | Clean, modular, and safe |
| Dev vs Ops | Devs control behavior | Ops/Admins control behavior |
📄 License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_plugin_system-1.1.0.tar.gz.
File metadata
- Download URL: django_plugin_system-1.1.0.tar.gz
- Upload date:
- Size: 16.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff70bda30dbd86e9d04f492a05dc972dcac1ed9afa508a985ccbfc076565a1f2
|
|
| MD5 |
8ce5b1e70564651fa1d2efaccac29376
|
|
| BLAKE2b-256 |
4498d0be2c10a5c80d1bd8e94f7ac333723573cd8e15086ce79c6aca3c5b4885
|
File details
Details for the file django_plugin_system-1.1.0-py3-none-any.whl.
File metadata
- Download URL: django_plugin_system-1.1.0-py3-none-any.whl
- Upload date:
- Size: 14.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71cb756a0ba51df097fb1f1fb6a3e0861cecb3394a3f75aa108134c3a063f2cb
|
|
| MD5 |
3ac87d4500c54cee55ed3a9e7ad28f04
|
|
| BLAKE2b-256 |
f156704cf38a4eef7aef7e5abb0c8d5b887e04a42f4e89842882bcc873699e59
|