Skip to content

Commit

Permalink
Merge pull request #22 from maykinmedia/feature/setup-configuration-w…
Browse files Browse the repository at this point in the history
…ith-subscriptions

Configuration step for notifications config and subscriptions
  • Loading branch information
swrichards authored Dec 12, 2024
2 parents 358555b + 57363d4 commit e4f138e
Show file tree
Hide file tree
Showing 15 changed files with 482 additions and 0 deletions.
1 change: 1 addition & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ python:
path: .
extra_requirements:
- db
- setup-configuration
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Features
:caption: Contents:

quickstart
setup_config


Indices and tables
Expand Down
58 changes: 58 additions & 0 deletions docs/setup_config.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Setup configuration
===================

Loading notification configuration from a YAML file
***************************************************

This library provides two ``ConfigurationStep`` implementations
(from the library ``django-setup-configuration``, see the
`documentation <https://github.com/maykinmedia/django-setup-configuration>`_
for more information on how to run ``setup_configuration``): one to configure the
service and retry settings, another to configure notification endpoint subscriptions.

To add these steps to your configuration steps, add ``django_setup_configuration``
to ``INSTALLED_APPS`` and add the following settings:

.. code:: python
SETUP_CONFIGURATION_STEPS = [
...
# Note the order: NotificationSubscriptionConfigurationStep expects a notification service
# to have been configured by NotificationConfigurationStep
"notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep"
"notifications_api_common.contrib.setup_configuration.steps.NotificationSubscriptionConfigurationStep"
...
]
The YAML file that is passed to ``setup_configuration`` must set the appropriate
flag and fields for both steps:

Example file:

.. code:: yaml
notifications_config_enable: True
notifications_config:
notifications_api_service_identifier: notifs-api
notification_delivery_max_retries: 1
notification_delivery_retry_backoff: 2
notification_delivery_retry_backoff_max: 3
notifications_subscriptions_config_enable: true
notifications_subscriptions_config:
items:
- identifier: my-subscription
callback_url: http://my/callback
client_id: the-client
secret: supersecret
uuid: 0f616bfd-aacc-4d85-a140-2af17a56217b
channels:
- Foo
- Bar
Because ``notifications_api_service_identifier`` is required, it might also be useful
to use the `ServiceConfigurationStep <https://zgw-consumers.readthedocs.io/en/latest/setup_config.html>`_
from ``zgw-consumers`` to configure the ``Service`` object for the notifications API.

Note that the ``uuid`` field in your subscriptions config must point to an existing
``Abonnement`` in the Open Notificaties service configured through ``notifications_config``.
Empty file.
Empty file.
46 changes: 46 additions & 0 deletions notifications_api_common/contrib/setup_configuration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django_setup_configuration.models import ConfigurationModel, DjangoModelRef
from pydantic import UUID4, Field

from notifications_api_common.models import NotificationsConfig, Subscription


class NotificationConfigurationModel(ConfigurationModel):
notifications_api_service_identifier: str = DjangoModelRef(
NotificationsConfig,
"notifications_api_service",
)

class Meta:
django_model_refs = {
NotificationsConfig: [
"notification_delivery_max_retries",
"notification_delivery_retry_backoff",
"notification_delivery_retry_backoff_max",
]
}


class SubscriptionConfigurationItem(ConfigurationModel):
uuid: UUID4 = Field(
description="The UUID for this subscription. Must match the UUID of the corresponding `Abonnement` in Open Notificaties."
)

channels: list[str] = DjangoModelRef(
Subscription,
"channels",
default_factory=list,
)

class Meta:
django_model_refs = {
Subscription: [
"identifier",
"callback_url",
"client_id",
"secret",
]
}


class SubscriptionConfigurationModel(ConfigurationModel):
items: list[SubscriptionConfigurationItem]
105 changes: 105 additions & 0 deletions notifications_api_common/contrib/setup_configuration/steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import logging

from django_setup_configuration.configuration import BaseConfigurationStep
from django_setup_configuration.exceptions import ConfigurationRunFailed
from furl import furl
from zgw_consumers.models import Service

from notifications_api_common.models import NotificationsConfig, Subscription

from .models import NotificationConfigurationModel, SubscriptionConfigurationModel

logger = logging.getLogger(__name__)


def get_service(slug: str) -> Service:
"""
Try to find a Service and re-raise DoesNotExist with the identifier
to make debugging easier
"""
try:
return Service.objects.get(slug=slug)
except Service.DoesNotExist as e:
raise Service.DoesNotExist(f"{str(e)} (identifier = {slug})")


class NotificationConfigurationStep(
BaseConfigurationStep[NotificationConfigurationModel]
):
"""
Configure settings for Notificaties API
"""

verbose_name = "Configuration for Notificaties API"
config_model = NotificationConfigurationModel
namespace = "notifications_config"
enable_setting = "notifications_config_enable"

def execute(self, model: NotificationConfigurationModel):
config = NotificationsConfig.get_solo()

if identifier := model.notifications_api_service_identifier:
config.notifications_api_service = get_service(identifier)

if model.notification_delivery_max_retries:
config.notification_delivery_max_retries = (
model.notification_delivery_max_retries
)
if model.notification_delivery_retry_backoff:
config.notification_delivery_retry_backoff = (
model.notification_delivery_retry_backoff
)
if model.notification_delivery_retry_backoff_max:
config.notification_delivery_retry_backoff_max = (
model.notification_delivery_retry_backoff_max
)

config.save()


class NotificationSubscriptionConfigurationStep(
BaseConfigurationStep[SubscriptionConfigurationModel]
):
"""
Configure settings for Notificaties API Subscriptions
"""

verbose_name = "Configuration for Notificaties API Subscriptions"
config_model = SubscriptionConfigurationModel
namespace = "notifications_subscriptions_config"
enable_setting = "notifications_subscriptions_config_enable"

def execute(self, model: SubscriptionConfigurationModel):
config = NotificationsConfig.get_solo()

if not (notifications_api := config.notifications_api_service):
raise ConfigurationRunFailed(
"No Notifications API Service configured. Please ensure you've first "
f"run {NotificationConfigurationStep.__name__}"
)

if len(model.items) == 0:
raise ConfigurationRunFailed("You must configure at least one subscription")

for item in model.items:
detail_url = furl(notifications_api.api_root)
detail_url.path /= f"/abonnement/{item.uuid!s}"
detail_url.path.normalize()

subscription, created = Subscription.objects.update_or_create(
identifier=item.identifier,
defaults={
"client_id": item.client_id,
"secret": item.secret,
"channels": item.channels,
"callback_url": item.callback_url,
"_subscription": str(detail_url),
},
)

logger.debug(
"%s subscription with identifier='%s' and pk='%s'",
"Created" if created else "Updated",
subscription.identifier,
subscription.pk,
)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ tests =
zgw-consumers[testutils]>=0.35.1
pep8 = flake8
coverage = pytest-cov
setup-configuration =
django-setup-configuration>=0.4.0
furl
docs =
sphinx
sphinx-rtd-theme
Expand Down
1 change: 1 addition & 0 deletions testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"simple_certmanager",
"zgw_consumers",
"notifications_api_common",
"django_setup_configuration",
"testapp",
]

Expand Down
6 changes: 6 additions & 0 deletions tests/files/setup_config_notifications_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
notifications_config_enable: true
notifications_config:
notifications_api_service_identifier: notifs-api
notification_delivery_max_retries: 1
notification_delivery_retry_backoff: 2
notification_delivery_retry_backoff_max: 3
5 changes: 5 additions & 0 deletions tests/files/setup_config_notifications_config_no_service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
notifications_config_enable: true
notifications_config:
notification_delivery_max_retries: 1
notification_delivery_retry_backoff: 2
notification_delivery_retry_backoff_max: 3
19 changes: 19 additions & 0 deletions tests/files/setup_config_subscriptions_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
notifications_subscriptions_config_enable: true
notifications_subscriptions_config:
items:
- identifier: my-subscription
callback_url: http://my/callback
client_id: the-client
secret: supersecret
uuid: 0f616bfd-aacc-4d85-a140-2af17a56217b
channels:
- Foo
- Bar
- identifier: my-other-subscription
callback_url: http://my/other-callback
client_id: the-client
secret: supersecret
uuid: a33cf110-06b6-454e-b7e9-5139c172ca9a
channels:
- Fuh
- Bahr
92 changes: 92 additions & 0 deletions tests/test_configuration_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import pytest
from django_setup_configuration.exceptions import PrerequisiteFailed
from django_setup_configuration.test_utils import execute_single_step
from zgw_consumers.test.factories import ServiceFactory

from notifications_api_common.contrib.setup_configuration.steps import (
NotificationConfigurationStep,
)
from notifications_api_common.models import NotificationsConfig

CONFIG_FILE_PATH = "tests/files/setup_config_notifications_config.yaml"
CONFIG_FILE_PATH_NO_SERVICE = (
"tests/files/setup_config_notifications_config_no_service.yaml"
)


@pytest.mark.django_db
def test_execute_configuration_step_success():
service = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)

execute_single_step(NotificationConfigurationStep, yaml_source=CONFIG_FILE_PATH)

config = NotificationsConfig.get_solo()

assert config.notifications_api_service == service
assert config.notification_delivery_max_retries == 1
assert config.notification_delivery_retry_backoff == 2
assert config.notification_delivery_retry_backoff_max == 3


@pytest.mark.django_db
def test_execute_configuration_step_update_existing():
service1 = ServiceFactory.create(
slug="other-api", api_root="http://other-notificaties.local/api/v1/"
)
service2 = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)

config = NotificationsConfig.get_solo()
config.notifications_api_service = service1
config.notification_delivery_max_retries = 1
config.notification_delivery_retry_backoff = 2
config.notification_delivery_retry_backoff_max = 3
config.save()

execute_single_step(NotificationConfigurationStep, yaml_source=CONFIG_FILE_PATH)

config = NotificationsConfig.get_solo()

assert config.notifications_api_service == service2
assert config.notification_delivery_max_retries == 1
assert config.notification_delivery_retry_backoff == 2
assert config.notification_delivery_retry_backoff_max == 3


@pytest.mark.django_db
def test_execute_configuration_step_without_service_success():
with pytest.raises(PrerequisiteFailed) as excinfo:
execute_single_step(
NotificationConfigurationStep, yaml_source=CONFIG_FILE_PATH_NO_SERVICE
)

assert (
"notifications_config.notifications_api_service_identifier\n Input should be a valid string"
in str(excinfo.value)
)


@pytest.mark.django_db
def test_execute_configuration_step_idempotent():
service = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)

def make_assertions():
config = NotificationsConfig.get_solo()

assert config.notifications_api_service == service
assert config.notification_delivery_max_retries == 1
assert config.notification_delivery_retry_backoff == 2
assert config.notification_delivery_retry_backoff_max == 3

execute_single_step(NotificationConfigurationStep, yaml_source=CONFIG_FILE_PATH)

make_assertions()

execute_single_step(NotificationConfigurationStep, yaml_source=CONFIG_FILE_PATH)

make_assertions()
Loading

0 comments on commit e4f138e

Please sign in to comment.