notifications-api-common 0.2.2__tar.gz → 0.3.1__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 (57) hide show
  1. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/CHANGELOG.rst +19 -0
  2. {notifications-api-common-0.2.2/notifications_api_common.egg-info → notifications_api_common-0.3.1}/PKG-INFO +33 -11
  3. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/README.rst +5 -4
  4. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/admin.py +1 -2
  5. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/kanalen.py +1 -0
  6. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/locale/en/LC_MESSAGES/django.po +1 -1
  7. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/locale/nl/LC_MESSAGES/django.po +1 -1
  8. notifications_api_common-0.3.1/notifications_api_common/management/commands/register_kanalen.py +137 -0
  9. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/models.py +12 -12
  10. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/settings.py +2 -0
  11. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/tasks.py +5 -5
  12. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/viewsets.py +8 -3
  13. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1/notifications_api_common.egg-info}/PKG-INFO +33 -11
  14. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common.egg-info/SOURCES.txt +1 -0
  15. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common.egg-info/requires.txt +4 -3
  16. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/setup.cfg +9 -9
  17. notifications_api_common-0.3.1/tests/test_register_kanalen.py +191 -0
  18. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/tests/test_register_webhook.py +15 -10
  19. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/tests/test_send_message.py +30 -12
  20. notifications-api-common-0.2.2/notifications_api_common/management/commands/register_kanalen.py +0 -82
  21. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/LICENSE +0 -0
  22. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/MANIFEST.in +0 -0
  23. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/__init__.py +0 -0
  24. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/api/__init__.py +0 -0
  25. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/api/serializers.py +0 -0
  26. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/apps.py +0 -0
  27. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/autoretry.py +0 -0
  28. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/constants.py +0 -0
  29. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/locale/en/LC_MESSAGES/django.mo +0 -0
  30. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/locale/nl/LC_MESSAGES/django.mo +0 -0
  31. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/management/__init__.py +0 -0
  32. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/management/commands/__init__.py +0 -0
  33. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0001_initial.py +0 -0
  34. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0002_notificationsconfig_notifications_api_service.py +0 -0
  35. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0003_migrate_api_root.py +0 -0
  36. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0004_remove_notificationsconfig_api_root.py +0 -0
  37. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0005_auto_20220810_0950.py +0 -0
  38. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0006_auto_20221018_1406.py +0 -0
  39. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0006_auto_20221213_0214.py +0 -0
  40. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0007_auto_20221206_0414.py +0 -0
  41. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/0008_merge_0006_auto_20221213_0214_0007_auto_20221206_0414.py +0 -0
  42. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/migrations/__init__.py +0 -0
  43. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/py.typed +0 -0
  44. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/query.py +0 -0
  45. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/templates/notifications_api_common/kanalen.html +0 -0
  46. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/tests/__init__.py +0 -0
  47. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/tests/utils.py +0 -0
  48. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/urls.py +0 -0
  49. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/utils.py +0 -0
  50. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/validators.py +0 -0
  51. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common/views.py +0 -0
  52. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common.egg-info/dependency_links.txt +0 -0
  53. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common.egg-info/not-zip-safe +0 -0
  54. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/notifications_api_common.egg-info/top_level.txt +0 -0
  55. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/pyproject.toml +0 -0
  56. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/setup.py +0 -0
  57. {notifications-api-common-0.2.2 → notifications_api_common-0.3.1}/tests/test_kanaal_documentation.py +0 -0
@@ -2,6 +2,25 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 0.3.1 (2024-10-27)
6
+ ------------------
7
+
8
+ * Fixed kanalen not being registered. This regression was introduced in `0.3.0`.
9
+
10
+ 0.3.0 (2024-10-24)
11
+ ------------------
12
+
13
+ * Upgraded Django requirement to >= 4.2
14
+ * Upgraded minimum python requirement to >= 3.10
15
+ * Added python 3.12 support
16
+ * Added the `NOTIFICATIONS_GUARANTEE_DELIVERY` environment variable which allows
17
+ `RuntimeError`s to be raised (or not to) whenever no Notifications API is
18
+ configured. The default for this setting is set to `True`.
19
+ * Upgraded zgw-consumers to 0.35.1
20
+ * This removed support for retrieving external OAS files to determine
21
+ * This requires `zgw_consumers.models.Service` ("service") instances to be
22
+ created to do external API calls through `zgw_consumer`
23
+
5
24
  0.2.2 (2023-04-20)
6
25
  ------------------
7
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: notifications-api-common
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Re-usable integration layer for the Common Ground Notifications API specification.
5
5
  Home-page: https://github.com/maykinmedia/notifications-api-common
6
6
  Author: Maykin Media
@@ -12,32 +12,53 @@ Project-URL: Source Code, https://github.com/maykinmedia/notifications-api-commo
12
12
  Keywords: notifications,REST,API,Common Ground,ZGW
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Framework :: Django
15
- Classifier: Framework :: Django :: 3.2
16
- Classifier: Framework :: Django :: 4.0
15
+ Classifier: Framework :: Django :: 4.2
17
16
  Classifier: Intended Audience :: Developers
18
17
  Classifier: Operating System :: Unix
19
18
  Classifier: Operating System :: MacOS
20
19
  Classifier: Operating System :: Microsoft :: Windows
21
- Classifier: Programming Language :: Python :: 3.7
22
- Classifier: Programming Language :: Python :: 3.8
23
- Classifier: Programming Language :: Python :: 3.9
24
20
  Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
25
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ License-File: LICENSE
25
+ Requires-Dist: django>=4.2.0
26
+ Requires-Dist: django-solo
27
+ Requires-Dist: djangorestframework>=3.12.0
28
+ Requires-Dist: celery
29
+ Requires-Dist: djangorestframework_camel_case>=1.2.0
30
+ Requires-Dist: zgw-consumers>=0.35.1
31
+ Requires-Dist: ape-pie
26
32
  Provides-Extra: db
33
+ Requires-Dist: psycopg2; extra == "db"
27
34
  Provides-Extra: tests
35
+ Requires-Dist: psycopg2; extra == "tests"
36
+ Requires-Dist: pytest; extra == "tests"
37
+ Requires-Dist: pytest-django; extra == "tests"
38
+ Requires-Dist: freezegun; extra == "tests"
39
+ Requires-Dist: tox; extra == "tests"
40
+ Requires-Dist: isort; extra == "tests"
41
+ Requires-Dist: black; extra == "tests"
42
+ Requires-Dist: flake8; extra == "tests"
43
+ Requires-Dist: zgw-consumers[testutils]>=0.35.1; extra == "tests"
28
44
  Provides-Extra: pep8
45
+ Requires-Dist: flake8; extra == "pep8"
29
46
  Provides-Extra: coverage
47
+ Requires-Dist: pytest-cov; extra == "coverage"
30
48
  Provides-Extra: docs
49
+ Requires-Dist: sphinx; extra == "docs"
50
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
31
51
  Provides-Extra: release
32
- License-File: LICENSE
52
+ Requires-Dist: bumpversion; extra == "release"
53
+ Requires-Dist: twine; extra == "release"
33
54
 
34
55
  notifications-api-common
35
56
  ========================
36
57
 
37
- :Version: 0.2.2
58
+ :Version: 0.3.1
38
59
  :Source: https://github.com/maykinmedia/notifications-api-common
39
60
  :Keywords: notifications, REST, API, Common Ground, ZGW
40
- :PythonVersion: 3.9
61
+ :PythonVersion: 3.10
41
62
 
42
63
  |build-status| |code-quality| |black| |coverage| |docs|
43
64
 
@@ -62,9 +83,10 @@ Installation
62
83
  Requirements
63
84
  ------------
64
85
 
65
- * Python 3.7 or above
86
+ * Python 3.10 or above
66
87
  * setuptools 30.3.0 or above
67
- * Django 3.2 or newer
88
+ * Django 4.2 or newer
89
+ * Celery 5.0 or newer setup with one worker deployed
68
90
 
69
91
 
70
92
  Install
@@ -1,10 +1,10 @@
1
1
  notifications-api-common
2
2
  ========================
3
3
 
4
- :Version: 0.2.2
4
+ :Version: 0.3.1
5
5
  :Source: https://github.com/maykinmedia/notifications-api-common
6
6
  :Keywords: notifications, REST, API, Common Ground, ZGW
7
- :PythonVersion: 3.9
7
+ :PythonVersion: 3.10
8
8
 
9
9
  |build-status| |code-quality| |black| |coverage| |docs|
10
10
 
@@ -29,9 +29,10 @@ Installation
29
29
  Requirements
30
30
  ------------
31
31
 
32
- * Python 3.7 or above
32
+ * Python 3.10 or above
33
33
  * setuptools 30.3.0 or above
34
- * Django 3.2 or newer
34
+ * Django 4.2 or newer
35
+ * Celery 5.0 or newer setup with one worker deployed
35
36
 
36
37
 
37
38
  Install
@@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
3
3
 
4
4
  from requests.exceptions import RequestException
5
5
  from solo.admin import SingletonModelAdmin
6
- from zds_client.client import ClientError
7
6
 
8
7
  from .models import NotificationsConfig, Subscription
9
8
 
@@ -20,7 +19,7 @@ def register_webhook(modeladmin, request, queryset):
20
19
 
21
20
  try:
22
21
  sub.register()
23
- except (ClientError, RequestException) as e:
22
+ except RequestException as e:
24
23
  messages.error(
25
24
  request,
26
25
  _(
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Provide notifications kanaal/exchange classes.
3
3
  """
4
+
4
5
  from collections import defaultdict
5
6
  from typing import Dict, Tuple
6
7
 
@@ -6,7 +6,7 @@
6
6
  #, fuzzy
7
7
  msgid ""
8
8
  msgstr ""
9
- "Project-Id-Version: 0.2.2\n"
9
+ "Project-Id-Version: 0.3.1\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
11
  "POT-Creation-Date: 2022-12-14 13:45+0100\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -6,7 +6,7 @@
6
6
  #, fuzzy
7
7
  msgid ""
8
8
  msgstr ""
9
- "Project-Id-Version: 0.2.2\n"
9
+ "Project-Id-Version: 0.3.1\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
11
  "POT-Creation-Date: 2022-12-14 13:45+0100\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -0,0 +1,137 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from django.contrib.sites.models import Site
5
+ from django.core.management.base import BaseCommand
6
+ from django.urls import reverse
7
+
8
+ from requests import Response
9
+ from requests.exceptions import JSONDecodeError, RequestException
10
+ from zgw_consumers.models import Service
11
+
12
+ from ...kanalen import KANAAL_REGISTRY
13
+ from ...models import NotificationsConfig
14
+ from ...settings import get_setting
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class KanaalException(Exception):
20
+ kanaal: str
21
+ data: dict | list
22
+ service: Service
23
+
24
+ def __init__(
25
+ self, kanaal: str, service: Service, data: Optional[dict | list] = None
26
+ ):
27
+ super().__init__()
28
+
29
+ self.kanaal = kanaal
30
+ self.service = service
31
+ self.data = data or {}
32
+
33
+
34
+ class KanaalRequestException(KanaalException):
35
+ def __str__(self) -> str:
36
+ return (
37
+ f"Unable to retrieve kanaal {self.kanaal} from {self.service}: {self.data}"
38
+ )
39
+
40
+
41
+ class KanaalCreateException(KanaalException):
42
+ def __str__(self) -> str:
43
+ return f"Unable to create kanaal {self.kanaal} at {self.service}: {self.data}"
44
+
45
+
46
+ class KanaalExistsException(KanaalException):
47
+ def __str__(self) -> str:
48
+ return f"Kanaal '{self.kanaal}' already exists within {self.service}"
49
+
50
+
51
+ def create_kanaal(kanaal: str, service: Service) -> None:
52
+ """
53
+ Create a kanaal, if it doesn't exist yet.
54
+ """
55
+ client = NotificationsConfig.get_client()
56
+
57
+ assert client
58
+
59
+ # look up the exchange in the registry
60
+ _kanaal = next(k for k in KANAAL_REGISTRY if k.label == kanaal)
61
+
62
+ response_data = []
63
+
64
+ try:
65
+ response: Response = client.get("kanaal", params={"naam": kanaal})
66
+ kanalen: list[dict] = response.json() or []
67
+ response.raise_for_status()
68
+ except (RequestException, JSONDecodeError) as exception:
69
+ raise KanaalRequestException(
70
+ kanaal=kanaal, service=service, data=response_data
71
+ ) from exception
72
+
73
+ if kanalen:
74
+ raise KanaalExistsException(kanaal=kanaal, service=service, data=response_data)
75
+
76
+ # build up own documentation URL
77
+ domain = Site.objects.get_current().domain
78
+ protocol = "https" if get_setting("IS_HTTPS") else "http"
79
+ documentation_url = (
80
+ f"{protocol}://{domain}{reverse('notifications:kanalen')}#{kanaal}"
81
+ )
82
+
83
+ try:
84
+ response: Response = client.post(
85
+ "kanaal",
86
+ json={
87
+ "naam": kanaal,
88
+ "documentatieLink": documentation_url,
89
+ "filters": list(_kanaal.kenmerken),
90
+ },
91
+ )
92
+
93
+ response_data: dict = response.json() or {}
94
+ response.raise_for_status()
95
+ except (RequestException, JSONDecodeError) as exception:
96
+ raise KanaalCreateException(
97
+ kanaal=kanaal, service=service, data=response_data
98
+ ) from exception
99
+
100
+
101
+ class Command(BaseCommand):
102
+ help = "Create kanaal in notification component"
103
+
104
+ def add_arguments(self, parser):
105
+ parser.add_argument(
106
+ "--kanalen",
107
+ nargs="*",
108
+ type=str,
109
+ help=(
110
+ "Names of the kanalen (will use the `KANAAL_REGISTRY` if not specified)"
111
+ ),
112
+ )
113
+
114
+ def handle(self, **options):
115
+ config = NotificationsConfig.get_solo()
116
+
117
+ if not config.notifications_api_service:
118
+ self.stderr.write(
119
+ "NotificationsConfig does not have a "
120
+ "`notifications_api_service` configured"
121
+ )
122
+
123
+ service = config.notifications_api_service
124
+
125
+ # use CLI arg or fall back to setting
126
+ kanalen = options["kanalen"] or sorted(
127
+ [kanaal.label for kanaal in KANAAL_REGISTRY]
128
+ )
129
+
130
+ for kanaal in kanalen:
131
+ try:
132
+ create_kanaal(kanaal, service)
133
+ self.stdout.write(
134
+ f"Registered kanaal '{kanaal}' with {service.api_root}"
135
+ )
136
+ except (KanaalException,) as exception:
137
+ self.stderr.write(f"{str(exception)} . Skipping..")
@@ -4,8 +4,9 @@ from django.contrib.postgres.fields import ArrayField
4
4
  from django.db import models
5
5
  from django.utils.translation import gettext_lazy as _
6
6
 
7
+ from ape_pie.client import APIClient
7
8
  from solo.models import SingletonModel
8
- from zds_client import Client, ClientAuth
9
+ from zgw_consumers.client import ZGWAuth, build_client
9
10
  from zgw_consumers.constants import APITypes
10
11
  from zgw_consumers.models import Service
11
12
 
@@ -56,13 +57,15 @@ class NotificationsConfig(SingletonModel):
56
57
  )
57
58
 
58
59
  @classmethod
59
- def get_client(cls) -> Optional[Client]:
60
+ def get_client(cls) -> Optional[APIClient]:
60
61
  """
61
62
  Construct a client, prepared with the required auth.
62
63
  """
63
64
  config = cls.get_solo()
64
65
  if config.notifications_api_service:
65
- return config.notifications_api_service.build_client()
66
+ return build_client(
67
+ config.notifications_api_service, client_factory=APIClient
68
+ )
66
69
  return None
67
70
 
68
71
 
@@ -110,23 +113,20 @@ class Subscription(models.Model):
110
113
  """
111
114
  Registers the webhook with the notification component.
112
115
  """
113
- assert (
114
- NotificationsConfig.get_solo().notifications_api_service
115
- ), "No service for Notifications API configured"
116
+ service = NotificationsConfig.get_solo().notifications_api_service
117
+ assert service, "No service for Notifications API configured"
116
118
 
117
119
  client = NotificationsConfig.get_client()
120
+ assert client
118
121
 
119
122
  # This authentication is for the NC to call us. Thus, it's *not* for
120
123
  # calling the NC to create a subscription.
121
124
  # TODO should be replaced with `TokenAuth`
122
125
  # see: maykinmedia/notifications-api-common/pull/1#discussion_r941450384
123
- self_auth = ClientAuth(
124
- client_id=self.client_id,
125
- secret=self.secret,
126
- )
126
+ self_auth = ZGWAuth(service)
127
127
  data = {
128
128
  "callbackUrl": self.callback_url,
129
- "auth": self_auth.credentials()["Authorization"],
129
+ "auth": f"Bearer {self_auth._token}",
130
130
  "kanalen": [
131
131
  {
132
132
  "naam": channel,
@@ -138,7 +138,7 @@ class Subscription(models.Model):
138
138
  }
139
139
 
140
140
  # register the subscriber
141
- subscriber = client.create("abonnement", data=data)
141
+ subscriber = client.post("abonnement", json=data).json()
142
142
 
143
143
  self._subscription = subscriber["url"]
144
144
  self.save(update_fields=["_subscription"])
@@ -7,6 +7,8 @@ NOTIFICATIONS_DISABLED = False
7
7
 
8
8
  IS_HTTPS = True
9
9
 
10
+ NOTIFICATIONS_GUARANTEE_DELIVERY = True
11
+
10
12
 
11
13
  def get_setting(name: str) -> Any:
12
14
  this_module = sys.modules[__name__]
@@ -2,7 +2,6 @@ import logging
2
2
 
3
3
  import requests
4
4
  from celery import shared_task
5
- from zds_client import ClientError
6
5
 
7
6
  from .autoretry import add_autoretry_behaviour
8
7
  from .models import NotificationsConfig
@@ -27,13 +26,14 @@ def send_notification(self, message: dict) -> None:
27
26
  return
28
27
 
29
28
  try:
30
- client.create("notificaties", message)
29
+ response = client.post("notificaties", json=message)
30
+ response.raise_for_status()
31
31
  # any unexpected errors should show up in error-monitoring, so we only
32
- # catch ClientError exceptions
33
- except (ClientError, requests.HTTPError) as exc:
32
+ # catch HTTPError exceptions
33
+ except requests.HTTPError as exc:
34
34
  logger.warning(
35
35
  "Could not deliver message to %s",
36
- client.api_root,
36
+ client.base_url,
37
37
  exc_info=exc,
38
38
  extra={
39
39
  "notification_msg": message,
@@ -161,11 +161,16 @@ class NotificationMixin(metaclass=NotificationMixinBase):
161
161
  # build the content of the notification
162
162
  message = self.construct_message(data, instance=instance)
163
163
 
164
- # build the client from the singleton config. This will raise an
165
- # exception if the config is not complete. We want this to hard-fail!
164
+ # build the client from the singleton config.
165
+ # This will raise an exception if the config is not complete unless
166
+ # NOTIFICATIONS_GUARANTEE_DELIVERY is explicitly set to False
166
167
  client = NotificationsConfig.get_client()
167
168
  if client is None:
168
- raise RuntimeError("Could not build a client for Notifications API")
169
+ msg = "Not notifying, Notifications API configuration is broken or absent."
170
+ logger.warning(msg)
171
+ if get_setting("NOTIFICATIONS_GUARANTEE_DELIVERY"):
172
+ raise RuntimeError(msg)
173
+ return
169
174
 
170
175
  # We've performed all the work that can raise uncaught exceptions that we can
171
176
  # still put inside an atomic transaction block. Next, we schedule the actual
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: notifications-api-common
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Re-usable integration layer for the Common Ground Notifications API specification.
5
5
  Home-page: https://github.com/maykinmedia/notifications-api-common
6
6
  Author: Maykin Media
@@ -12,32 +12,53 @@ Project-URL: Source Code, https://github.com/maykinmedia/notifications-api-commo
12
12
  Keywords: notifications,REST,API,Common Ground,ZGW
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Framework :: Django
15
- Classifier: Framework :: Django :: 3.2
16
- Classifier: Framework :: Django :: 4.0
15
+ Classifier: Framework :: Django :: 4.2
17
16
  Classifier: Intended Audience :: Developers
18
17
  Classifier: Operating System :: Unix
19
18
  Classifier: Operating System :: MacOS
20
19
  Classifier: Operating System :: Microsoft :: Windows
21
- Classifier: Programming Language :: Python :: 3.7
22
- Classifier: Programming Language :: Python :: 3.8
23
- Classifier: Programming Language :: Python :: 3.9
24
20
  Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
25
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ License-File: LICENSE
25
+ Requires-Dist: django>=4.2.0
26
+ Requires-Dist: django-solo
27
+ Requires-Dist: djangorestframework>=3.12.0
28
+ Requires-Dist: celery
29
+ Requires-Dist: djangorestframework_camel_case>=1.2.0
30
+ Requires-Dist: zgw-consumers>=0.35.1
31
+ Requires-Dist: ape-pie
26
32
  Provides-Extra: db
33
+ Requires-Dist: psycopg2; extra == "db"
27
34
  Provides-Extra: tests
35
+ Requires-Dist: psycopg2; extra == "tests"
36
+ Requires-Dist: pytest; extra == "tests"
37
+ Requires-Dist: pytest-django; extra == "tests"
38
+ Requires-Dist: freezegun; extra == "tests"
39
+ Requires-Dist: tox; extra == "tests"
40
+ Requires-Dist: isort; extra == "tests"
41
+ Requires-Dist: black; extra == "tests"
42
+ Requires-Dist: flake8; extra == "tests"
43
+ Requires-Dist: zgw-consumers[testutils]>=0.35.1; extra == "tests"
28
44
  Provides-Extra: pep8
45
+ Requires-Dist: flake8; extra == "pep8"
29
46
  Provides-Extra: coverage
47
+ Requires-Dist: pytest-cov; extra == "coverage"
30
48
  Provides-Extra: docs
49
+ Requires-Dist: sphinx; extra == "docs"
50
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
31
51
  Provides-Extra: release
32
- License-File: LICENSE
52
+ Requires-Dist: bumpversion; extra == "release"
53
+ Requires-Dist: twine; extra == "release"
33
54
 
34
55
  notifications-api-common
35
56
  ========================
36
57
 
37
- :Version: 0.2.2
58
+ :Version: 0.3.1
38
59
  :Source: https://github.com/maykinmedia/notifications-api-common
39
60
  :Keywords: notifications, REST, API, Common Ground, ZGW
40
- :PythonVersion: 3.9
61
+ :PythonVersion: 3.10
41
62
 
42
63
  |build-status| |code-quality| |black| |coverage| |docs|
43
64
 
@@ -62,9 +83,10 @@ Installation
62
83
  Requirements
63
84
  ------------
64
85
 
65
- * Python 3.7 or above
86
+ * Python 3.10 or above
66
87
  * setuptools 30.3.0 or above
67
- * Django 3.2 or newer
88
+ * Django 4.2 or newer
89
+ * Celery 5.0 or newer setup with one worker deployed
68
90
 
69
91
 
70
92
  Install
@@ -50,5 +50,6 @@ notifications_api_common/templates/notifications_api_common/kanalen.html
50
50
  notifications_api_common/tests/__init__.py
51
51
  notifications_api_common/tests/utils.py
52
52
  tests/test_kanaal_documentation.py
53
+ tests/test_register_kanalen.py
53
54
  tests/test_register_webhook.py
54
55
  tests/test_send_message.py
@@ -1,10 +1,10 @@
1
- django>=3.2.0
1
+ django>=4.2.0
2
2
  django-solo
3
3
  djangorestframework>=3.12.0
4
4
  celery
5
5
  djangorestframework_camel_case>=1.2.0
6
- gemma-zds-client>=0.15.0
7
- zgw-consumers
6
+ zgw-consumers>=0.35.1
7
+ ape-pie
8
8
 
9
9
  [coverage]
10
10
  pytest-cov
@@ -32,3 +32,4 @@ tox
32
32
  isort
33
33
  black
34
34
  flake8
35
+ zgw-consumers[testutils]>=0.35.1
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = notifications-api-common
3
- version = 0.2.2
3
+ version = 0.3.1
4
4
  description = Re-usable integration layer for the Common Ground Notifications API specification.
5
5
  long_description = file: README.rst
6
6
  url = https://github.com/maykinmedia/notifications-api-common
@@ -15,16 +15,14 @@ keywords = notifications, REST, API, Common Ground, ZGW
15
15
  classifiers =
16
16
  Development Status :: 4 - Beta
17
17
  Framework :: Django
18
- Framework :: Django :: 3.2
19
- Framework :: Django :: 4.0
18
+ Framework :: Django :: 4.2
20
19
  Intended Audience :: Developers
21
20
  Operating System :: Unix
22
21
  Operating System :: MacOS
23
22
  Operating System :: Microsoft :: Windows
24
- Programming Language :: Python :: 3.7
25
- Programming Language :: Python :: 3.8
26
- Programming Language :: Python :: 3.9
27
23
  Programming Language :: Python :: 3.10
24
+ Programming Language :: Python :: 3.11
25
+ Programming Language :: Python :: 3.12
28
26
  Topic :: Software Development :: Libraries :: Python Modules
29
27
 
30
28
  [options]
@@ -32,13 +30,13 @@ zip_safe = False
32
30
  include_package_data = True
33
31
  packages = find:
34
32
  install_requires =
35
- django>=3.2.0
33
+ django>=4.2.0
36
34
  django-solo
37
35
  djangorestframework>=3.12.0
38
36
  celery
39
37
  djangorestframework_camel_case>=1.2.0
40
- gemma-zds-client>=0.15.0
41
- zgw-consumers
38
+ zgw-consumers>=0.35.1
39
+ ape-pie
42
40
  tests_require =
43
41
  psycopg2
44
42
  pytest
@@ -48,6 +46,7 @@ tests_require =
48
46
  isort
49
47
  black
50
48
  flake8
49
+ zgw-consumers[testutils]>=0.35.1
51
50
 
52
51
  [options.packages.find]
53
52
  include =
@@ -66,6 +65,7 @@ tests =
66
65
  isort
67
66
  black
68
67
  flake8
68
+ zgw-consumers[testutils]>=0.35.1
69
69
  pep8 = flake8
70
70
  coverage = pytest-cov
71
71
  docs =
@@ -0,0 +1,191 @@
1
+ from io import StringIO
2
+ from unittest.mock import Mock, patch
3
+ from urllib.parse import urlencode
4
+
5
+ from django.test.testcases import call_command
6
+
7
+ import pytest
8
+
9
+ from notifications_api_common.kanalen import KANAAL_REGISTRY, Kanaal
10
+
11
+ kanalen = set(
12
+ (
13
+ Kanaal(label="foobar", main_resource=Mock()),
14
+ Kanaal(label="boofar", main_resource=Mock()),
15
+ )
16
+ )
17
+
18
+ KANAAL_REGISTRY.clear()
19
+ KANAAL_REGISTRY.update(kanalen)
20
+
21
+
22
+ @pytest.mark.django_db
23
+ def test_register_kanalen_success(notifications_config, requests_mock):
24
+ kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
25
+ params = urlencode(dict(naam="foobar"))
26
+
27
+ requests_mock.get(f"{kanaal_url}?{params}", json=[])
28
+
29
+ requests_mock.post(
30
+ kanaal_url,
31
+ json={
32
+ "url": "http://example.com",
33
+ "naam": "string",
34
+ "documentatieLink": "http://example.com",
35
+ "filters": ["string"],
36
+ },
37
+ status_code=201,
38
+ )
39
+
40
+ reverse_patch = (
41
+ "notifications_api_common.management.commands.register_kanalen.reverse"
42
+ )
43
+
44
+ with patch(reverse_patch) as mocked_reverse:
45
+ mocked_reverse.return_value = "/notifications/kanalen"
46
+
47
+ call_command("register_kanalen", kanalen=["foobar"])
48
+
49
+ assert len(requests_mock.request_history) == 2
50
+
51
+ get_request = requests_mock.request_history[0]
52
+
53
+ assert get_request._request.url == f"{kanaal_url}?{params}"
54
+
55
+ post_request = requests_mock.request_history[1]
56
+
57
+ assert post_request._request.url == kanaal_url
58
+
59
+
60
+ @pytest.mark.django_db
61
+ def test_register_kanalen_from_registry_success(notifications_config, requests_mock):
62
+ kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
63
+
64
+ url_mapping = {
65
+ kanaal.label: f"{kanaal_url}?{urlencode(dict(naam=kanaal.label))}"
66
+ for kanaal in kanalen
67
+ }
68
+
69
+ for kanaal in kanalen:
70
+ requests_mock.get(url_mapping[kanaal.label], json=[])
71
+
72
+ requests_mock.post(
73
+ kanaal_url,
74
+ json={
75
+ "url": "http://example.com",
76
+ "naam": kanaal.label,
77
+ "documentatieLink": "http://example.com",
78
+ "filters": ["string"],
79
+ },
80
+ status_code=201,
81
+ )
82
+
83
+ reverse_patch = (
84
+ "notifications_api_common.management.commands.register_kanalen.reverse"
85
+ )
86
+
87
+ with patch(reverse_patch) as mocked_reverse:
88
+ mocked_reverse.return_value = "/notifications/kanalen"
89
+
90
+ call_command("register_kanalen")
91
+
92
+ assert len(requests_mock.request_history) == 4
93
+
94
+ # kanalen are sorted by label
95
+ first_get_request = requests_mock.request_history[0]
96
+ assert first_get_request._request.url == url_mapping["boofar"]
97
+
98
+ first_post_request = requests_mock.request_history[1]
99
+ assert first_post_request._request.url == kanaal_url
100
+
101
+ second_get_request = requests_mock.request_history[2]
102
+ assert second_get_request._request.url == url_mapping["foobar"]
103
+
104
+ second_post_request = requests_mock.request_history[3]
105
+ assert second_post_request._request.url == kanaal_url
106
+
107
+
108
+ @pytest.mark.django_db
109
+ def test_register_kanalen_existing_kanalen(notifications_config, requests_mock):
110
+ """
111
+ Test that already registered kanalen does not cause issues
112
+ """
113
+ kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
114
+ params = urlencode(dict(naam="foobar"))
115
+
116
+ requests_mock.get(
117
+ f"{kanaal_url}?{params}",
118
+ json=[
119
+ {
120
+ "url": "http://example.com",
121
+ "naam": "foobar",
122
+ "documentatieLink": "http://example.com",
123
+ "filters": ["string"],
124
+ }
125
+ ],
126
+ )
127
+
128
+ call_command("register_kanalen", kanalen=["foobar"])
129
+
130
+ assert len(requests_mock.request_history) == 1
131
+
132
+ request = requests_mock.request_history[0]
133
+
134
+ assert request._request.url == f"{kanaal_url}?{params}"
135
+
136
+
137
+ @pytest.mark.django_db
138
+ def test_register_kanalen_unknown_url(notifications_config, requests_mock):
139
+ kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
140
+ params = urlencode(dict(naam="foobar"))
141
+
142
+ requests_mock.get(f"{kanaal_url}?{params}", status_code=404)
143
+
144
+ stderr = StringIO()
145
+
146
+ call_command("register_kanalen", kanalen=["foobar"], stderr=stderr)
147
+
148
+ partial_failure_message = "Unable to retrieve kanaal foobar"
149
+
150
+ assert partial_failure_message in stderr.getvalue()
151
+
152
+ assert len(requests_mock.request_history) == 1
153
+
154
+ request = requests_mock.request_history[0]
155
+
156
+ assert request._request.url == f"{kanaal_url}?{params}"
157
+
158
+
159
+ @pytest.mark.django_db
160
+ def test_register_kanalen_incorrect_post(notifications_config, requests_mock):
161
+ kanaal_url = f"{notifications_config.notifications_api_service.api_root}kanaal"
162
+ params = urlencode(dict(naam="foobar"))
163
+
164
+ requests_mock.get(f"{kanaal_url}?{params}", json=[])
165
+
166
+ requests_mock.post(kanaal_url, json={"error": "foo"}, status_code=400)
167
+
168
+ stderr = StringIO()
169
+
170
+ reverse_patch = (
171
+ "notifications_api_common.management.commands.register_kanalen.reverse"
172
+ )
173
+
174
+ with patch(reverse_patch) as mocked_reverse:
175
+ mocked_reverse.return_value = "/notifications/kanalen"
176
+
177
+ call_command("register_kanalen", kanalen=["foobar"], stderr=stderr)
178
+
179
+ partial_failure_message = "Unable to create kanaal foobar"
180
+
181
+ assert partial_failure_message in stderr.getvalue()
182
+
183
+ assert len(requests_mock.request_history) == 2
184
+
185
+ get_request = requests_mock.request_history[0]
186
+
187
+ assert get_request._request.url == f"{kanaal_url}?{params}"
188
+
189
+ post_request = requests_mock.request_history[1]
190
+
191
+ assert post_request._request.url == kanaal_url
@@ -4,16 +4,23 @@ from django.contrib.messages import get_messages
4
4
  from django.utils.translation import gettext as _
5
5
 
6
6
  import pytest
7
- from requests.exceptions import RequestException
8
- from zds_client.client import ClientError
7
+ from requests.exceptions import HTTPError, RequestException
9
8
 
10
9
  from notifications_api_common.admin import register_webhook
11
10
  from notifications_api_common.models import Subscription
12
11
 
13
12
 
13
+ class MockResponse:
14
+ def __init__(self, data):
15
+ self.data = data
16
+
17
+ def json(self):
18
+ return self.data
19
+
20
+
14
21
  @patch(
15
- "zds_client.client.Client.create",
16
- return_value={"url": "https://example.com/api/v1/abonnementen/1"},
22
+ "ape_pie.APIClient.post",
23
+ return_value=MockResponse({"url": "https://example.com/api/v1/abonnementen/1"}),
17
24
  )
18
25
  @pytest.mark.django_db
19
26
  def test_register_webhook_success(
@@ -47,9 +54,7 @@ def test_register_webhook_request_exception(
47
54
  channels=["zaken"],
48
55
  )
49
56
 
50
- with patch(
51
- "zds_client.client.Client.create", side_effect=RequestException("exception")
52
- ):
57
+ with patch("ape_pie.APIClient.post", side_effect=RequestException("exception")):
53
58
  register_webhook(object, request_with_middleware, Subscription.objects.all())
54
59
 
55
60
  messages = list(get_messages(request_with_middleware))
@@ -61,7 +66,7 @@ def test_register_webhook_request_exception(
61
66
 
62
67
 
63
68
  @pytest.mark.django_db
64
- def test_register_webhook_client_error(request_with_middleware, notifications_config):
69
+ def test_register_webhook_http_error(request_with_middleware, notifications_config):
65
70
  Subscription.objects.create(
66
71
  callback_url="https://example.com/callback",
67
72
  client_id="client_id",
@@ -69,7 +74,7 @@ def test_register_webhook_client_error(request_with_middleware, notifications_co
69
74
  channels=["zaken"],
70
75
  )
71
76
 
72
- with patch("zds_client.client.Client.create", side_effect=ClientError("exception")):
77
+ with patch("ape_pie.APIClient.post", side_effect=HTTPError("400")):
73
78
  register_webhook(object, request_with_middleware, Subscription.objects.all())
74
79
 
75
80
  messages = list(get_messages(request_with_middleware))
@@ -77,4 +82,4 @@ def test_register_webhook_client_error(request_with_middleware, notifications_co
77
82
  assert len(messages) == 1
78
83
  assert messages[0].message == _(
79
84
  "Something went wrong while registering subscription for {callback_url}: {e}"
80
- ).format(callback_url="https://example.com/callback", e="exception")
85
+ ).format(callback_url="https://example.com/callback", e="400")
@@ -1,12 +1,12 @@
1
1
  from pathlib import Path
2
2
  from unittest.mock import patch
3
3
 
4
+ from django.test import override_settings
4
5
  from django.urls import reverse
5
6
 
6
7
  import pytest
7
8
  from celery.exceptions import Retry
8
9
  from freezegun import freeze_time
9
- from zgw_consumers.test import mock_service_oas_get
10
10
 
11
11
  from notifications_api_common.tasks import NotificationException, send_notification
12
12
  from testapp.models import Person
@@ -50,9 +50,6 @@ def test_api_create_person(api_client, notifications_config):
50
50
  @pytest.mark.django_db()
51
51
  def test_task_send_notification_success(notifications_config, requests_mock, settings):
52
52
  msg = {"foo": "bar"}
53
- # add mocks
54
- settings.ZGW_CONSUMERS_TEST_SCHEMA_DIRS = [TESTS_DIR / "schemas"]
55
- mock_service_oas_get(requests_mock, NOTIFICATIONS_API_ROOT, "nrc")
56
53
  requests_mock.post(f"{NOTIFICATIONS_API_ROOT}notificaties", status_code=201)
57
54
 
58
55
  send_notification(msg)
@@ -68,10 +65,6 @@ def test_task_send_notification_with_retry(
68
65
  notifications_config, requests_mock, settings
69
66
  ):
70
67
  msg = {"foo": "bar"}
71
-
72
- # add NRC mocks
73
- settings.ZGW_CONSUMERS_TEST_SCHEMA_DIRS = [TESTS_DIR / "schemas"]
74
- mock_service_oas_get(requests_mock, NOTIFICATIONS_API_ROOT, "nrc")
75
68
  exc = NotificationException()
76
69
  requests_mock.post(f"{NOTIFICATIONS_API_ROOT}notificaties", exc=exc)
77
70
 
@@ -93,8 +86,6 @@ def test_task_send_notification_catch_404(
93
86
  which can be handled by the lib users
94
87
  """
95
88
  # add NRC mocks
96
- settings.ZGW_CONSUMERS_TEST_SCHEMA_DIRS = [TESTS_DIR / "schemas"]
97
- mock_service_oas_get(requests_mock, NOTIFICATIONS_API_ROOT, "nrc")
98
89
  requests_mock.post(f"{NOTIFICATIONS_API_ROOT}notificaties", status_code=404)
99
90
 
100
91
  with pytest.raises(NotificationException):
@@ -110,9 +101,36 @@ def test_task_send_notification_catch_500(
110
101
  which can be handled by the lib users
111
102
  """
112
103
  # add NRC mocks
113
- settings.ZGW_CONSUMERS_TEST_SCHEMA_DIRS = [TESTS_DIR / "schemas"]
114
- mock_service_oas_get(requests_mock, NOTIFICATIONS_API_ROOT, "nrc")
115
104
  requests_mock.post(f"{NOTIFICATIONS_API_ROOT}notificaties", status_code=500)
116
105
 
117
106
  with pytest.raises(NotificationException):
118
107
  send_notification({"foo": "bar"})
108
+
109
+
110
+ @freeze_time("2022-01-01")
111
+ @pytest.mark.django_db(transaction=True)
112
+ def test_api_create_person_unconfigured(api_client, notifications_config):
113
+ """Test the default behavior of NOTIFICATIONS_GUARANTEE_DELIVERY=True"""
114
+ notifications_config.delete()
115
+ url = reverse("person-list")
116
+ data = {"name": "John", "address_street": "Grotestraat", "address_number": "1"}
117
+
118
+ with pytest.raises(RuntimeError):
119
+ response = api_client.post(url, data)
120
+
121
+ assert Person.objects.count() == 0
122
+
123
+
124
+ @freeze_time("2022-01-01")
125
+ @pytest.mark.django_db(transaction=True)
126
+ @override_settings(NOTIFICATIONS_GUARANTEE_DELIVERY=False)
127
+ def test_api_create_person_unconfigured_notifications_guarantee_delivery_false(
128
+ api_client, notifications_config
129
+ ):
130
+ notifications_config.delete()
131
+ url = reverse("person-list")
132
+ data = {"name": "John", "address_street": "Grotestraat", "address_number": "1"}
133
+
134
+ response = api_client.post(url, data)
135
+
136
+ assert Person.objects.count() == 1
@@ -1,82 +0,0 @@
1
- import logging
2
-
3
- from django.contrib.sites.models import Site
4
- from django.core.management.base import BaseCommand
5
- from django.urls import reverse
6
-
7
- from ...kanalen import KANAAL_REGISTRY
8
- from ...models import NotificationsConfig
9
- from ...settings import get_setting
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class KanaalExists(Exception):
15
- pass
16
-
17
-
18
- def create_kanaal(kanaal: str) -> None:
19
- """
20
- Create a kanaal, if it doesn't exist yet.
21
- """
22
- client = NotificationsConfig.get_client()
23
-
24
- # look up the exchange in the registry
25
- _kanaal = next(k for k in KANAAL_REGISTRY if k.label == kanaal)
26
-
27
- kanalen = client.list("kanaal", query_params={"naam": kanaal})
28
- if kanalen:
29
- raise KanaalExists()
30
-
31
- # build up own documentation URL
32
- domain = Site.objects.get_current().domain
33
- protocol = "https" if get_setting("IS_HTTPS") else "http"
34
- documentation_url = (
35
- f"{protocol}://{domain}{reverse('notifications:kanalen')}#{kanaal}"
36
- )
37
-
38
- client.create(
39
- "kanaal",
40
- {
41
- "naam": kanaal,
42
- "documentatieLink": documentation_url,
43
- "filters": list(_kanaal.kenmerken),
44
- },
45
- )
46
-
47
-
48
- class Command(BaseCommand):
49
- help = "Create kanaal in notification component"
50
-
51
- def add_arguments(self, parser):
52
- parser.add_argument(
53
- "--kanalen",
54
- nargs="*",
55
- type=str,
56
- help=(
57
- "Names of the kanalen (will use the `KANAAL_REGISTRY` if not specified)"
58
- ),
59
- )
60
-
61
- def handle(self, **options):
62
- config = NotificationsConfig.get_solo()
63
-
64
- if not config.notifications_api_service:
65
- self.stderr.write(
66
- "NotificationsConfig does not have a "
67
- "`notifications_api_service` configured"
68
- )
69
-
70
- api_root = config.notifications_api_service.api_root
71
-
72
- # use CLI arg or fall back to setting
73
- kanalen = options["kanalen"] or sorted(
74
- [kanaal.label for kanaal in KANAAL_REGISTRY]
75
- )
76
-
77
- for kanaal in kanalen:
78
- try:
79
- create_kanaal(kanaal)
80
- self.stdout.write(f"Registered kanaal '{kanaal}' with {api_root}")
81
- except KanaalExists:
82
- self.stderr.write(f"Kanaal '{kanaal}' already exists within {api_root}")