Skip to content

Commit

Permalink
Added subscription and observers module.
Browse files Browse the repository at this point in the history
  • Loading branch information
Tonye Jack committed Oct 20, 2019
1 parent fadbc19 commit 4405e25
Show file tree
Hide file tree
Showing 28 changed files with 441 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
.idea/

*.sqlite3
*.pyc
__pycache__/

*.egg-info/
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include *.py
include *.txt
recursive-include django_model_subscription *.py
recursive-include model_subscription *.py
File renamed without changes.
3 changes: 3 additions & 0 deletions demo/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
8 changes: 8 additions & 0 deletions demo/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig


class DemoConfig(AppConfig):
name = 'demo'

def ready(self):
from demo import subscription
26 changes: 26 additions & 0 deletions demo/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 2.2.6 on 2019-10-20 16:27

from django.db import migrations, models
import model_subscription.mixin


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='TestModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20)),
],
options={
'abstract': False,
},
bases=(model_subscription.mixin.SubscriptionModelMixin, models.Model),
),
]
File renamed without changes.
8 changes: 8 additions & 0 deletions demo/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.db import models

# Create your models here.
from model_subscription.models import SubscriptionModel


class TestModel(SubscriptionModel):
name = models.CharField(max_length=20)
16 changes: 16 additions & 0 deletions demo/subscription.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from demo.models import TestModel
from model_subscription.constants import OperationType
from model_subscription.decorators import subscribe, create_subscription, unsubscribe_create


@subscribe(OperationType.CREATE, TestModel)
def handle_create_1(instance):
print('1. Created {}'.format(instance.name))


@create_subscription(TestModel)
def handle_create_2(instance):
print('2. Created {}'.format(instance.name))


unsubscribe_create(TestModel, handle_create_2)
18 changes: 18 additions & 0 deletions demo/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.test import TestCase

# Create your tests here.
import os

import django
from django.conf import settings

# Create your tests here.

if __name__ == '__main__':
os.environ['DJANGO_SETTINGS_MODULE'] = 'django_model_subscription.settings'
django.setup()

from demo.models import TestModel


t = TestModel.objects.create(name='test')
File renamed without changes.
Binary file not shown.
Binary file not shown.
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Django settings for django_model_subcription project.
Django settings for django_model_subscription project.
Generated by 'django-admin startproject' using Django 2.0.2.
Expand Down Expand Up @@ -37,6 +37,8 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'model_subscription',
'demo',
]

MIDDLEWARE = [
Expand All @@ -49,7 +51,7 @@
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'django_model_subcription.urls'
ROOT_URLCONF = 'django_model_subscription.urls'

TEMPLATES = [
{
Expand All @@ -67,7 +69,9 @@
},
]

WSGI_APPLICATION = 'django_model_subcription.wsgi.application'
WSGI_APPLICATION = 'django_model_subscription.wsgi.application'

SUBSCRIPTION_MODULE = 'subscription'


# Database
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""django_model_subcription URL Configuration
"""django_model_subscription URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
WSGI config for django_model_subcription project.
WSGI config for django_model_subscription project.
It exposes the WSGI callable as a module-level variable named ``application``.
Expand All @@ -11,6 +11,6 @@

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_model_subcription.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_model_subscription.settings")

application = get_wsgi_application()
2 changes: 1 addition & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_model_subcription.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_model_subscription.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
Expand Down
3 changes: 3 additions & 0 deletions model_subscription/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VERSION = (0, 0, 1)

__version__ = '.'.join(map(str, VERSION))
7 changes: 7 additions & 0 deletions model_subscription/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class OperationType(str, Enum):
CREATE = 'create'
UPDATE = 'update'
DELETE = 'delete'
69 changes: 69 additions & 0 deletions model_subscription/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from functools import partial
from typing import Callable, Optional

from django.db import models

from model_subscription.constants import OperationType
from model_subscription.mixin import SubscriptionModelMixin

"""
@subscribe(OperationType.CREATE, TestModel)
def my_custom_receiver(instance)
pass
@subscribe(OperationType.UPDATE, TestModel)
def my_custom_receiver(instance, updated_data)
pass
@subscribe(OperationType.DELETE, TestModel)
def my_custom_receiver(instance)
pass
"""

"""
@create_subscription(TestModel)
def my_custom_receiver(instance)
pass
@update_subscription(TestModel)
def my_custom_receiver(instance, updated_data)
pass
@delete_subscription(TestModel)
def my_custom_receiver(test_instance)
pass
"""


def subscribe(operation, model):
# type: ((SubscriptionModelMixin, models.Model), OperationType) -> Callable
def _decorator(func):
model._subscription.attach(operation, func)
return func
return _decorator


create_subscription = partial(subscribe, OperationType.CREATE)
update_subscription = partial(subscribe, OperationType.UPDATE)
delete_subscription = partial(subscribe, OperationType.DELETE)


def unsubscribe(operation, model, func=None):
# type: ((SubscriptionModelMixin, models.Model), OperationType, Optional[Callable]) -> Callable

if func is not None:
model._subscription.detach(operation, func)
return func

def _decorator(inner):
model._subscription.detach(operation, inner)
return inner
return _decorator


unsubscribe_create = partial(unsubscribe, OperationType.CREATE)
unsubscribe_update = partial(unsubscribe, OperationType.UPDATE)
unsubscribe_delete = partial(unsubscribe, OperationType.DELETE)
42 changes: 42 additions & 0 deletions model_subscription/mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.db.models.base import ModelBase
from django.utils import six
from django_lifecycle import LifecycleModelMixin, hook

from model_subscription.constants import OperationType
from model_subscription.subscriber import ModelSubscription


class SubscriptionMeta(ModelBase):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""

def __new__(cls, name, bases, attrs):
for base in bases:
if hasattr(bases, '_subscription'):
del base['_subscription']
_subscription = ModelSubscription()
attrs['_subscription'] = _subscription
return super(SubscriptionMeta, cls).__new__(cls, name, bases, attrs)


@six.add_metaclass(SubscriptionMeta)
class SubscriptionModelMixin(LifecycleModelMixin):
def __init__(self, *args, **kwargs):
self._subscription.subscription_model = self
self._subscription.auto_discover()
super(SubscriptionModelMixin, self).__init__(*args, **kwargs)

@hook('after_create')
def notify_create(self):
self._subscription.notify(OperationType.CREATE)

@hook('after_update')
def notify_update(self):
self._subscription.notify(OperationType.UPDATE)

@hook('after_delete')
def notify_delete(self):
self._subscription.notify(OperationType.DELETE)
7 changes: 6 additions & 1 deletion model_subscription/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from django.db import models

# Create your models here.
from model_subscription.mixin import SubscriptionModelMixin


class SubscriptionModel(SubscriptionModelMixin, models.Model):
class Meta:
abstract = True
78 changes: 78 additions & 0 deletions model_subscription/observers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from abc import ABC, abstractmethod
from typing import Callable, Any, List, Tuple, Union

from django.db import models

from model_subscription.constants import OperationType


class Observer(ABC):
"""
The Observer interface declares the update method.
"""

def __init__(self):
self._receivers = [] # type: List[Tuple[int, Callable[[models.Model, dict], Any]]]

@property
@abstractmethod
def action(self):
pass

@abstractmethod
def handle(self, instance, changed_data):
# type: (models.Model, dict) -> None
"""
Receive update from subject.
"""
pass

@property
def receivers(self):
return self._receivers

@receivers.setter
def receivers(self, other):
# type: (Union[Callable, list]) -> None
if isinstance(other, list):
for receiver in other:
if id(receiver) not in [x[0] for x in self._receivers]:
self._receivers.append((id(receiver), receiver))
else:
if id(other) not in [x[0] for x in self._receivers]:
self._receivers.append((id(other), other))

@receivers.deleter
def receivers(self):
self._receivers = []

"""
Concrete Observers react to the operations issued by the Model they have been attached to.
"""


class CreateObserver(Observer):
action = OperationType.CREATE

def handle(self, instance, changed_data):
# type: (models.Model, dict) -> None
for _, receiver in self.receivers:
receiver(instance)


class UpdateObserver(Observer):
action = OperationType.UPDATE

def handle(self, instance, changed_data):
# type: (models.Model, dict) -> None
for _, receiver in self.receivers:
receiver(instance, changed_data)


class DeleteObserver(Observer):
action = OperationType.DELETE

def handle(self, instance, changed_data):
# type: (models.Model, dict) -> None
for _, receiver in self.receivers:
receiver(instance)

0 comments on commit 4405e25

Please sign in to comment.