Skip to main content

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 syncpluginsync management command (and optional post_migrate signal).
  • Admin UX — filter/search, bulk enable/disable, and quick priority nudges.
  • Deterministic selection — pick a single plugin by status and priority, with caching.
  • Safe defaultsget_or_create syncing 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: PluginType and PluginItem.


🔌 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 active item.
  • Fallback: if no active, pick the lowest-priority reserve item.
  • Cache: the chosen item is cached per PluginType and 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:

  1. Automatically after migrations (default)

    • The post_migrate signal syncs in create-only mode (preserves admin edits).
  2. 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 → uses get_or_create (safe: won’t overwrite status/priority changed in Admin)
  • update → uses update_or_create (refresh description/priority from code)

🧭 Admin Panel

  • PluginType list shows counts per status.
  • PluginItem list lets you:
    • quick-edit status and priority,
    • bulk mark ACTIVE/RESERVED/DISABLED,
    • Increase/Decrease priority in-place,
    • view a “Class loads” boolean to catch broken registrations.

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 create in test setup to materialize rows.
  • Toggle item status in 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

  1. You define one interface:
from abc import ABC, abstractmethod

class AbstractNotifier(ABC):
    @abstractmethod
    def send(self, user, message): ...
  1. 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",
})
  1. 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",
})
  1. You can now dynamically select from Admin which notifiers are active, in reserve, or disabled — even reorder them by priority.
  2. 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

MIT Alireza Tabatabaeian

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

django_plugin_system-1.1.0.tar.gz (16.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_plugin_system-1.1.0-py3-none-any.whl (14.7 kB view details)

Uploaded Python 3

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

Hashes for django_plugin_system-1.1.0.tar.gz
Algorithm Hash digest
SHA256 ff70bda30dbd86e9d04f492a05dc972dcac1ed9afa508a985ccbfc076565a1f2
MD5 8ce5b1e70564651fa1d2efaccac29376
BLAKE2b-256 4498d0be2c10a5c80d1bd8e94f7ac333723573cd8e15086ce79c6aca3c5b4885

See more details on using hashes here.

File details

Details for the file django_plugin_system-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_plugin_system-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 71cb756a0ba51df097fb1f1fb6a3e0861cecb3394a3f75aa108134c3a063f2cb
MD5 3ac87d4500c54cee55ed3a9e7ad28f04
BLAKE2b-256 f156704cf38a4eef7aef7e5abb0c8d5b887e04a42f4e89842882bcc873699e59

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page