django-plugin-system 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. django_plugin_system-1.0.0/LICENSE +7 -0
  2. django_plugin_system-1.0.0/PKG-INFO +400 -0
  3. django_plugin_system-1.0.0/README.md +365 -0
  4. django_plugin_system-1.0.0/django_plugin_system.egg-info/PKG-INFO +400 -0
  5. django_plugin_system-1.0.0/django_plugin_system.egg-info/SOURCES.txt +21 -0
  6. django_plugin_system-1.0.0/django_plugin_system.egg-info/dependency_links.txt +1 -0
  7. django_plugin_system-1.0.0/django_plugin_system.egg-info/requires.txt +9 -0
  8. django_plugin_system-1.0.0/django_plugin_system.egg-info/top_level.txt +1 -0
  9. django_plugin_system-1.0.0/pyproject.toml +75 -0
  10. django_plugin_system-1.0.0/setup.cfg +4 -0
  11. django_plugin_system-1.0.0/src/django_plugin_system/__init__.py +4 -0
  12. django_plugin_system-1.0.0/src/django_plugin_system/admin.py +64 -0
  13. django_plugin_system-1.0.0/src/django_plugin_system/apps.py +10 -0
  14. django_plugin_system-1.0.0/src/django_plugin_system/helpers.py +28 -0
  15. django_plugin_system-1.0.0/src/django_plugin_system/managment/__init__.py +0 -0
  16. django_plugin_system-1.0.0/src/django_plugin_system/managment/commands/__init__.py +0 -0
  17. django_plugin_system-1.0.0/src/django_plugin_system/managment/commands/pluginsync.py +32 -0
  18. django_plugin_system-1.0.0/src/django_plugin_system/models.py +126 -0
  19. django_plugin_system-1.0.0/src/django_plugin_system/register.py +47 -0
  20. django_plugin_system-1.0.0/src/django_plugin_system/services/__init__.py +1 -0
  21. django_plugin_system-1.0.0/src/django_plugin_system/services/sync.py +84 -0
  22. django_plugin_system-1.0.0/src/django_plugin_system/signals.py +14 -0
  23. django_plugin_system-1.0.0/src/django_plugin_system/storage.py +27 -0
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Alireza Tabatabaeian
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-plugin-system
3
+ Version: 1.0.0
4
+ Summary: A lightweight plugin registry for Django with admin management and registry→DB sync.
5
+ Author-email: Alireza Tabatabaeian <alireza.tabatabaeian@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Alireza-Tabatabaeian/django-plugin-system
7
+ Project-URL: Issues, https://github.com/Alireza-Tabatabaeian/django-plugin-system/issues
8
+ Keywords: django,plugins,registry,otp,extensibility
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.0
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: Django>=4.2
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: pytest-django>=4.8.0; extra == "dev"
30
+ Requires-Dist: mypy>=1.8; extra == "dev"
31
+ Requires-Dist: django-stubs>=5.0.0; extra == "dev"
32
+ Requires-Dist: black>=24.0.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.5.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # Django Plugin System
37
+
38
+ A lightweight, batteries-included plugin registry for Django apps.
39
+ 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**.
40
+
41
+ ---
42
+
43
+ ## ✨ Features
44
+
45
+ - **Simple interface-first design** — define an abstract base class (ABC), register implementations.
46
+ - **Registry → DB sync** — `pluginsync` management command (and optional `post_migrate` signal).
47
+ - **Admin UX** — filter/search, bulk enable/disable, and quick priority nudges.
48
+ - **Deterministic selection** — pick a single plugin by `status` and `priority`, with caching.
49
+ - **Safe defaults** — `get_or_create` syncing preserves admin-edited fields.
50
+ - **Uniqueness guarantees** — unique constraints for types and items.
51
+
52
+ ---
53
+
54
+ ## 📦 Installation
55
+
56
+ ```bash
57
+ pip install django-plugin-system
58
+ ```
59
+
60
+ Add the app to `INSTALLED_APPS`:
61
+
62
+ ```python
63
+ # settings.py
64
+ INSTALLED_APPS = [
65
+ # ...
66
+ "django_plugin_system",
67
+ ]
68
+ ```
69
+
70
+ > The app creates two models: `PluginType` and `PluginItem`.
71
+
72
+ ---
73
+
74
+ ## 🔌 Core Concepts
75
+
76
+ ### 1) Define an **interface** (ABC)
77
+
78
+ ```python
79
+ # apps/otp/interfaces.py
80
+ from abc import ABC, abstractmethod
81
+
82
+ class AbstractOTP(ABC):
83
+ @abstractmethod
84
+ def send_otp(self, number: str, code: str) -> None: ...
85
+ ```
86
+
87
+ ### 2) Register a **plugin type**
88
+
89
+ ```python
90
+ # apps/otp/apps.py (or any module imported at startup)
91
+ from django.apps import AppConfig
92
+ from django_plugin_system.register import register_plugin_type
93
+
94
+ class OtpConfig(AppConfig):
95
+ name = "apps.otp"
96
+ def ready(self):
97
+ register_plugin_type({
98
+ "name": "otp",
99
+ "manager": self.name, # the app providing the type
100
+ "interface": AbstractOTP,
101
+ "description": "One-time password (OTP) delivery channel",
102
+ })
103
+ ```
104
+
105
+ ### 3) Implement and register **plugin items**
106
+
107
+ ```python
108
+ # apps/otp_sms/plugins.py
109
+ from django_plugin_system.register import register_plugin_item
110
+ from .interfaces import AbstractOTP
111
+
112
+ class SmsIrOTP(AbstractOTP):
113
+ def send_otp(self, number: str, code: str) -> None:
114
+ # call provider api...
115
+ pass
116
+
117
+ # Register this implementation
118
+ register_plugin_item({
119
+ "name": "sms_ir",
120
+ "module": "apps.otp_sms", # the app providing the item
121
+ "type_name": "otp",
122
+ "manager_name": "apps.otp", # the app providing the type
123
+ "plugin_class": SmsIrOTP,
124
+ "priority": 10,
125
+ "description": "Send OTP using Sms.ir provider",
126
+ })
127
+ ```
128
+
129
+ > You can register multiple items with different priorities. Lower number means **higher** priority.
130
+
131
+ ---
132
+
133
+ ## 🗄️ Database Models
134
+
135
+ ```python
136
+ # django_plugin_system.models
137
+ class PluginType(models.Model):
138
+ id = UUID PK
139
+ name = CharField
140
+ manager = CharField # the app that defines the interface
141
+ description = TextField
142
+
143
+ class PluginItem(models.Model):
144
+ id = UUID PK
145
+ plugin_type = FK -> PluginType
146
+ module = CharField # the app that provides the implementation
147
+ name = CharField
148
+ status = TextChoices('active', 'reserve', 'disable')
149
+ priority = SmallIntegerField # lower is better
150
+ description = TextField
151
+ ```
152
+
153
+ ### Uniqueness
154
+
155
+ - `PluginType(name, manager)` is unique.
156
+ - `PluginItem(name, module, plugin_type)` is unique.
157
+
158
+ ---
159
+
160
+ ## 🧠 Selection Logic
161
+
162
+ - **Active first:** pick the lowest-priority `active` item.
163
+ - **Fallback:** if no `active`, pick the lowest-priority `reserve` item.
164
+ - **Cache:** the chosen item is cached per `PluginType` and auto‑invalidated on save/delete.
165
+
166
+ ### Helper
167
+
168
+ ```python
169
+ from django_plugin_system.helpers import get_plugin_instance
170
+
171
+ otp = get_plugin_instance("otp", "apps.otp")
172
+ if otp:
173
+ otp.send_otp("+31123456789", "123456")
174
+ ```
175
+
176
+ > Or call `PluginType.get_single_plugin()` then `.load_class()` to instantiate manually.
177
+
178
+ ---
179
+
180
+ ## 🛠️ Syncing the Registry
181
+
182
+ You have **two** ways to keep the DB aligned with the in‑memory registry:
183
+
184
+ 1) **Automatically after migrations** (default)
185
+ - The `post_migrate` signal syncs in *create-only* mode (preserves admin edits).
186
+
187
+ 2) **Manually via command**
188
+ ```bash
189
+ python manage.py pluginsync
190
+ # or to refresh defaults from registry (overwrites description/priority on conflicts):
191
+ python manage.py pluginsync --mode update
192
+ # skip pruning of stale rows:
193
+ python manage.py pluginsync --no-prune
194
+ ```
195
+
196
+ **Modes:**
197
+
198
+ - `create` → uses `get_or_create` (safe: won’t overwrite `status`/`priority` changed in Admin)
199
+ - `update` → uses `update_or_create` (refresh `description`/`priority` from code)
200
+
201
+ ---
202
+
203
+ ## 🧭 Admin Panel
204
+
205
+ - **PluginType** list shows counts per status.
206
+ - **PluginItem** list lets you:
207
+ - quick-edit `status` and `priority`,
208
+ - bulk mark **ACTIVE/RESERVED/DISABLED**,
209
+ - **Increase/Decrease priority** in-place,
210
+ - view a “Class loads” boolean to catch broken registrations.
211
+
212
+ > Changing status/priority automatically clears the single‑plugin selection cache.
213
+
214
+ ---
215
+
216
+ ## 🔄 Overriding selection
217
+
218
+ You can override the selection logic per type by providing a `get_plugin` callable in the type registry entry:
219
+
220
+ ```python
221
+ def my_selector(plugin_type_model_obj):
222
+ # your custom logic (possibly data-driven)
223
+ return plugin_type_model_obj.get_active_plugins()[0]
224
+
225
+ register_plugin_type({
226
+ "name": "otp",
227
+ "manager": "apps.otp",
228
+ "interface": AbstractOTP,
229
+ "description": "OTP delivery",
230
+ "get_plugin": my_selector, # <- override
231
+ })
232
+ ```
233
+
234
+ ---
235
+
236
+ ## 🧪 Testing tips
237
+
238
+ - Ensure your registry code paths are imported in test settings (e.g., via `AppConfig.ready`).
239
+ - Use `pluginsync --mode create` in test setup to materialize rows.
240
+ - Toggle item `status` in tests to verify fallback and cache invalidation.
241
+
242
+ ---
243
+
244
+ ## 📐 Design Notes & Guarantees
245
+
246
+ - Registry data lives in memory at import time; DB represents a **materialized view** used by Admin and runtime selection.
247
+ - Syncing is **idempotent** and safe to run many times.
248
+ - Items are validated to **implement the declared interface**.
249
+ - Errors on registration are not swallowed — mis-registrations fail early and loudly.
250
+
251
+ ---
252
+
253
+ ## 🤔 Why Use Django Plugin System?
254
+
255
+ 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.
256
+
257
+ 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.
258
+
259
+ ---
260
+
261
+ ### 🧩 Example: Notification System
262
+
263
+ Imagine you have multiple ways to notify users:
264
+
265
+ - Email
266
+
267
+ - SMS
268
+
269
+ - Push notification
270
+
271
+ Each of these is handled by a different piece of code — maybe even from different apps.
272
+
273
+ ### 🚫 **Without** Django Plugin System
274
+
275
+ - You hardcode your imports and logic:
276
+
277
+ ```python
278
+ # notifications/core.py
279
+ from notifications.email_sender import send_email
280
+ from notifications.sms_sender import send_sms
281
+ from notifications.push_sender import send_push
282
+
283
+ def notify_user(user, message):
284
+ if user.prefers_email:
285
+ send_email(user.email, message)
286
+ elif user.prefers_sms:
287
+ send_sms(user.phone, message)
288
+ elif user.prefers_push:
289
+ send_push(user.device_token, message)
290
+ ```
291
+ - Every time you add a new provider, you must:
292
+ - Write new import statements
293
+ - Modify your logic
294
+ - Re-deploy your code
295
+ - Possibly break something that used to work
296
+
297
+ And there’s no way for an admin to change behavior dynamically — everything is baked into code.
298
+
299
+ ---
300
+ ### ✅ **With** Django Plugin System
301
+
302
+ 1. You define one interface:
303
+
304
+ ```python
305
+ from abc import ABC, abstractmethod
306
+
307
+ class AbstractNotifier(ABC):
308
+ @abstractmethod
309
+ def send(self, user, message): ...
310
+ ```
311
+
312
+ 2. You register it as a plugin type:
313
+
314
+ ```python
315
+ from django_plugin_system.register import register_plugin_type
316
+
317
+ register_plugin_type({
318
+ "name": "notifier",
319
+ "manager": "apps.notifications",
320
+ "interface": AbstractNotifier,
321
+ "description": "Notification channels for users",
322
+ })
323
+ ```
324
+
325
+ 3. You implement as many plugin items as you want (completely independent of the rest of the code):
326
+
327
+ ```python
328
+ from django_plugin_system.register import register_plugin_item
329
+ from .interfaces import AbstractNotifier
330
+
331
+ class EmailNotifier(AbstractNotifier):
332
+ def send(self, user, message):
333
+ print(f"Sending email to {user.email}: {message}")
334
+
335
+ register_plugin_item({
336
+ "name": "email",
337
+ "module": "apps.notifications.email",
338
+ "type_name": "notifier",
339
+ "manager_name": "apps.notifications",
340
+ "plugin_class": EmailNotifier,
341
+ "priority": 5,
342
+ "description": "Send notification via Email",
343
+ })
344
+ ```
345
+
346
+ 4. You can now dynamically select from Admin which notifiers are active, in reserve, or disabled — even reorder them by priority.
347
+ 5. Your code doesn’t change at all:
348
+
349
+ ```python
350
+ from django_plugin_system.helpers import get_plugin_instance
351
+
352
+ notifier = get_plugin_instance("notifier", "apps.notifications")
353
+ notifier.send(user, "Your OTP is 1234")
354
+ ```
355
+
356
+ 💡 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.
357
+
358
+ ```python
359
+ # models.py
360
+ from typing import List
361
+
362
+ from django.db import models
363
+ from django.contrib.auth.models import User
364
+
365
+ from django_plugin_system.models import PluginItem
366
+
367
+
368
+ class UserNotifyPref(models.Model):
369
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
370
+ favourite_plugins = models.ManyToManyField(PluginItem)
371
+
372
+ @staticmethod
373
+ def get_user_plugins(user: User) -> List[PluginItem]:
374
+ try:
375
+ return list(UserNotifyPref.objects.get(user=user).favourite_plugins.all())
376
+ except UserNotifyPref.DoesNotExist:
377
+ return []
378
+ ```
379
+
380
+ Now the following code will notify user through all selected plugins:
381
+ ```python
382
+ from myapp.models import UserNotifyPref
383
+
384
+ ...
385
+
386
+ favourite_plugins = UserNotifyPref.get_user_plugins(user)
387
+ for plugin in favourite_plugins:
388
+ plugin_service = plugin.load_class()
389
+ plugin_service.send(user, message)
390
+
391
+ ...
392
+
393
+ ```
394
+ just as simple as you see, **with** django-plugin-system, you can let users decide which plugin they prefer to use.
395
+
396
+ ---
397
+
398
+ ## 📄 License
399
+
400
+ MIT [Alireza Tabatabaeian](https://github.com/Alireza-Tabatabaeian)