-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from maykinmedia/feature/setup-configuration-w…
…ith-subscriptions Configuration step for notifications config and subscriptions
- Loading branch information
Showing
15 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ python: | |
path: . | ||
extra_requirements: | ||
- db | ||
- setup-configuration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ Features | |
:caption: Contents: | ||
|
||
quickstart | ||
setup_config | ||
|
||
|
||
Indices and tables | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
notifications_api_common/contrib/setup_configuration/models.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
105
notifications_api_common/contrib/setup_configuration/steps.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
5
tests/files/setup_config_notifications_config_no_service.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.