edx-ace 1.11.4__tar.gz → 1.12.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 (64) hide show
  1. {edx-ace-1.11.4/edx_ace.egg-info → edx-ace-1.12.0}/PKG-INFO +1 -1
  2. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/__init__.py +1 -1
  3. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/__init__.py +3 -0
  4. edx-ace-1.12.0/edx_ace/channel/braze_push_notification.py +64 -0
  5. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/presentation.py +1 -0
  6. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/renderers.py +15 -0
  7. edx-ace-1.12.0/edx_ace/tests/channel/test_braze_push_notification.py +105 -0
  8. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_policy.py +10 -6
  9. edx-ace-1.12.0/edx_ace/tests/utils/test_braze_utils.py +59 -0
  10. edx-ace-1.12.0/edx_ace/utils/braze.py +27 -0
  11. {edx-ace-1.11.4 → edx-ace-1.12.0/edx_ace.egg-info}/PKG-INFO +1 -1
  12. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/SOURCES.txt +4 -0
  13. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/entry_points.txt +1 -0
  14. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/requires.txt +1 -0
  15. {edx-ace-1.11.4 → edx-ace-1.12.0}/requirements/base.in +1 -0
  16. {edx-ace-1.11.4 → edx-ace-1.12.0}/setup.py +36 -6
  17. {edx-ace-1.11.4 → edx-ace-1.12.0}/CHANGELOG.rst +0 -0
  18. {edx-ace-1.11.4 → edx-ace-1.12.0}/LICENSE.txt +0 -0
  19. {edx-ace-1.11.4 → edx-ace-1.12.0}/MANIFEST.in +0 -0
  20. {edx-ace-1.11.4 → edx-ace-1.12.0}/README.rst +0 -0
  21. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/ace.py +0 -0
  22. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/apps.py +0 -0
  23. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/braze.py +0 -0
  24. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/django_email.py +0 -0
  25. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/file.py +0 -0
  26. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/mixins.py +0 -0
  27. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/push_notification.py +0 -0
  28. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/channel/sailthru.py +0 -0
  29. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/delivery.py +0 -0
  30. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/errors.py +0 -0
  31. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/message.py +0 -0
  32. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/monitoring.py +0 -0
  33. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/policy.py +0 -0
  34. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/push_notifications/views/__init__.py +0 -0
  35. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/recipient.py +0 -0
  36. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/recipient_resolver.py +0 -0
  37. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/serialization.py +0 -0
  38. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/signals.py +0 -0
  39. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/templatetags/acetags.py +0 -0
  40. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/test_utils/__init__.py +0 -0
  41. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_braze.py +0 -0
  42. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_channel_helpers.py +0 -0
  43. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_django_email.py +0 -0
  44. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_file_email.py +0 -0
  45. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_push_notification.py +0 -0
  46. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/channel/test_sailthru.py +0 -0
  47. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_ace.py +0 -0
  48. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_date.py +0 -0
  49. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_delivery.py +0 -0
  50. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_message.py +0 -0
  51. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_presentation.py +0 -0
  52. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/body.html +0 -0
  53. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/head.html +0 -0
  54. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/tests/utils/test_signals.py +0 -0
  55. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/utils/__init__.py +0 -0
  56. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/utils/date.py +0 -0
  57. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/utils/once.py +0 -0
  58. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/utils/plugins.py +0 -0
  59. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace/utils/signals.py +0 -0
  60. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/dependency_links.txt +0 -0
  61. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/not-zip-safe +0 -0
  62. {edx-ace-1.11.4 → edx-ace-1.12.0}/edx_ace.egg-info/top_level.txt +0 -0
  63. {edx-ace-1.11.4 → edx-ace-1.12.0}/requirements/constraints.txt +0 -0
  64. {edx-ace-1.11.4 → edx-ace-1.12.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-ace
3
- Version: 1.11.4
3
+ Version: 1.12.0
4
4
  Summary: Framework for Messaging
5
5
  Home-page: https://github.com/openedx/edx-ace
6
6
  Author: edX
@@ -13,7 +13,7 @@ from .policy import Policy, PolicyResult
13
13
  from .recipient import Recipient
14
14
  from .recipient_resolver import RecipientResolver
15
15
 
16
- __version__ = '1.11.4'
16
+ __version__ = '1.12.0'
17
17
 
18
18
 
19
19
  __all__ = [
@@ -28,6 +28,7 @@ class ChannelType(Enum):
28
28
 
29
29
  EMAIL = 'email'
30
30
  PUSH = 'push'
31
+ BRAZE_PUSH = 'braze_push'
31
32
 
32
33
  def __str__(self):
33
34
  return str(self.value)
@@ -184,6 +185,8 @@ def get_channel_for_message(channel_type, message):
184
185
  channel_names = [settings.ACE_CHANNEL_DEFAULT_EMAIL]
185
186
  elif channel_type == ChannelType.PUSH and getattr(settings, "ACE_CHANNEL_DEFAULT_PUSH", None):
186
187
  channel_names = [settings.ACE_CHANNEL_DEFAULT_PUSH]
188
+ elif channel_type == ChannelType.BRAZE_PUSH and getattr(settings, "ACE_CHANNEL_BRAZE_PUSH", None):
189
+ channel_names = [settings.ACE_CHANNEL_BRAZE_PUSH]
187
190
 
188
191
  try:
189
192
  possible_channels = [
@@ -0,0 +1,64 @@
1
+ """
2
+ Channel for sending push notifications using braze.
3
+ """
4
+ import logging
5
+
6
+ from django.conf import settings
7
+
8
+ from edx_ace.channel import Channel, ChannelType
9
+ from edx_ace.message import Message
10
+ from edx_ace.renderers import RenderedPushNotification
11
+ from edx_ace.utils.braze import get_braze_client
12
+
13
+ LOG = logging.getLogger(__name__)
14
+
15
+
16
+ class BrazePushNotificationChannel(Channel):
17
+ """
18
+ A channel for sending push notifications using braze.
19
+ """
20
+ channel_type = ChannelType.BRAZE_PUSH
21
+ _CAMPAIGNS_SETTING = 'ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS'
22
+
23
+ @classmethod
24
+ def enabled(cls):
25
+ """
26
+ Returns: True iff braze client is available.
27
+ """
28
+ return bool(get_braze_client())
29
+
30
+ def deliver(self, message: Message, rendered_message: RenderedPushNotification) -> None:
31
+ """
32
+ Transmit a rendered message to a recipient.
33
+
34
+ Args:
35
+ message: The message to transmit.
36
+ rendered_message: The rendered content of the message that has been personalized
37
+ for this particular recipient.
38
+ """
39
+ braze_campaign = message.options['braze_campaign']
40
+ emails = message.options.get('emails') or [message.recipient.email_address]
41
+ campaign_id = self._campaign_id(braze_campaign)
42
+ if not campaign_id:
43
+ LOG.info('Could not find braze campaign for notification %s', braze_campaign)
44
+ return
45
+
46
+ try:
47
+ braze_client = get_braze_client()
48
+ braze_client.send_campaign_message(
49
+ campaign_id=campaign_id,
50
+ trigger_properties=message.context['post_data'],
51
+ emails=emails
52
+ )
53
+ LOG.info('Sent push notification for %s with Braze', braze_campaign)
54
+ except Exception as exc: # pylint: disable=broad-except
55
+ LOG.error(
56
+ 'Unable to send push notification for %s with Braze. Reason: %s',
57
+ braze_campaign,
58
+ str(exc)
59
+ )
60
+
61
+ @classmethod
62
+ def _campaign_id(cls, braze_campaign):
63
+ """Returns the campaign ID for a given ACE message name or None if no match is found"""
64
+ return getattr(settings, cls._CAMPAIGNS_SETTING, {}).get(braze_campaign)
@@ -10,6 +10,7 @@ from edx_ace.channel import ChannelType
10
10
  RENDERERS = {
11
11
  ChannelType.EMAIL: renderers.EmailRenderer(),
12
12
  ChannelType.PUSH: renderers.PushNotificationRenderer(),
13
+ ChannelType.BRAZE_PUSH: renderers.BrazePushNotificationRenderer(),
13
14
  }
14
15
 
15
16
 
@@ -99,3 +99,18 @@ class PushNotificationRenderer(AbstractRenderer):
99
99
  A renderer for :attr:`.ChannelType.PUSH` channels.
100
100
  """
101
101
  rendered_message_cls = RenderedPushNotification
102
+
103
+
104
+ @attr.s
105
+ class RenderedBrazePushNotification:
106
+ """
107
+ Encapsulates all values needed to send a :class:`.Message`
108
+ over an :attr:`.ChannelType.BRAZE_PUSH`.
109
+ """
110
+
111
+
112
+ class BrazePushNotificationRenderer(AbstractRenderer):
113
+ """
114
+ A renderer for :attr:`.ChannelType.PUSH` channels.
115
+ """
116
+ rendered_message_cls = RenderedBrazePushNotification
@@ -0,0 +1,105 @@
1
+ """
2
+ Tests for TestBrazePushNotificationChannel.
3
+ """
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from django.contrib.auth import get_user_model
9
+ from django.test import TestCase, override_settings
10
+
11
+ from edx_ace.channel.braze_push_notification import BrazePushNotificationChannel
12
+ from edx_ace.message import Message
13
+ from edx_ace.recipient import Recipient
14
+ from edx_ace.renderers import RenderedBrazePushNotification
15
+
16
+ BRAZE_URL = "https://example.braze.com"
17
+ API_KEY = "test-api-key"
18
+ User = get_user_model()
19
+
20
+
21
+ @pytest.mark.django_db
22
+ @override_settings(
23
+ EDX_BRAZE_API_KEY=API_KEY,
24
+ EDX_BRAZE_API_SERVER=BRAZE_URL,
25
+ )
26
+ class TestBrazePushNotificationChannel(TestCase):
27
+
28
+ def setUp(self):
29
+ super().setUp()
30
+ self.user = User.objects.create(username='username', email='email@example.com')
31
+ self.lms_user_id = self.user.id
32
+ self.mocked_post_data = {
33
+ 'notification_type': 'new_response',
34
+ 'course_id': 'course-v1:edX+DemoX+Demo_Course',
35
+ 'content_url': 'http://localhost',
36
+ 'replier_name': 'verified',
37
+ 'post_title': 'New test response',
38
+ 'course_name': 'Demonstration Course',
39
+ 'thread_id': '67bedeb9ceb0b101343294c5',
40
+ 'topic_id': 'i4x-edx-eiorguegnru-course-foobarbaz',
41
+ 'response_id': '67ffa1f1ceb0b10134db3d8e',
42
+ 'comment_id': None,
43
+ 'strong': 'strong', 'p': 'p'
44
+ }
45
+
46
+ self.mocked_payload = {
47
+ 'campaign_id': '1234test',
48
+ 'trigger_properties': self.mocked_post_data,
49
+ 'emails': ['edx@example.com']
50
+ }
51
+
52
+ @patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=True)
53
+ def test_enabled(self, mock_braze_client):
54
+ """
55
+ Test that the channel is enabled when the settings are configured.
56
+ """
57
+ assert BrazePushNotificationChannel.enabled()
58
+
59
+ @patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=False)
60
+ def test_disabled(self, mock_braze_client):
61
+ """
62
+ Test that the channel is disabled when the settings are not configured.
63
+ """
64
+ assert not BrazePushNotificationChannel.enabled()
65
+
66
+ @override_settings(ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS={'new_response': "1234test"})
67
+ @patch('edx_ace.channel.braze_push_notification.get_braze_client')
68
+ def test_deliver_success(self, mock_braze_function):
69
+ mock_braze_client = MagicMock()
70
+ mock_braze_function.return_value = mock_braze_client
71
+ mock_braze_client.send_campaign_message = MagicMock(return_value=True)
72
+
73
+ rendered_message = RenderedBrazePushNotification()
74
+ message = Message(
75
+ app_label='testapp',
76
+ name="test_braze",
77
+ recipient=Recipient(lms_user_id="1", email_address="user@example.com"),
78
+ context={'post_data': self.mocked_post_data},
79
+ options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'}
80
+ )
81
+ channel = BrazePushNotificationChannel()
82
+ channel.deliver(message, rendered_message)
83
+ mock_braze_client.send_campaign_message.assert_called_once()
84
+ args, kwargs = mock_braze_client.send_campaign_message.call_args
85
+
86
+ # Verify the payload
87
+ self.assertEqual(kwargs, self.mocked_payload)
88
+
89
+ @patch('edx_ace.channel.braze_push_notification.get_braze_client')
90
+ def test_campaign_not_configured(self, mock_braze_function):
91
+ mock_braze_client = MagicMock()
92
+ mock_braze_function.return_value = mock_braze_client
93
+ mock_braze_client.send_campaign_message = MagicMock(return_value=True)
94
+
95
+ rendered_message = RenderedBrazePushNotification()
96
+ message = Message(
97
+ app_label='testapp',
98
+ name="test_braze",
99
+ recipient=Recipient(lms_user_id="1", email_address="user@example.com"),
100
+ context={'post_data': self.mocked_post_data},
101
+ options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'}
102
+ )
103
+ channel = BrazePushNotificationChannel()
104
+ channel.deliver(message, rendered_message)
105
+ mock_braze_client.send_campaign_message.assert_not_called()
@@ -24,12 +24,16 @@ class TestPolicy(TestCase):
24
24
  PolicyCase(deny_values=[set(ChannelType)], expected_channels=set()),
25
25
 
26
26
  # deny only email
27
- PolicyCase(deny_values=[{ChannelType.EMAIL}], expected_channels={ChannelType.PUSH}), # single policy
28
- PolicyCase(deny_values=[{ChannelType.EMAIL}, set()], expected_channels={ChannelType.PUSH}), # multiple policies
29
-
30
- # deny both email and push
31
- PolicyCase(deny_values=[{ChannelType.EMAIL, ChannelType.PUSH}], expected_channels=set()), # single policy
32
- PolicyCase(deny_values=[{ChannelType.EMAIL}, {ChannelType.PUSH}], expected_channels=set()), # multiple policies
27
+ PolicyCase(deny_values=[{ChannelType.EMAIL}],
28
+ expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # single policy
29
+ PolicyCase(deny_values=[{ChannelType.EMAIL}, set()],
30
+ expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # multiple policies
31
+
32
+ # deny email, push and braze_push
33
+ PolicyCase(deny_values=[{ChannelType.EMAIL, ChannelType.PUSH, ChannelType.BRAZE_PUSH}],
34
+ expected_channels=set()), # single policy
35
+ PolicyCase(deny_values=[{ChannelType.EMAIL}, {ChannelType.PUSH},
36
+ {ChannelType.BRAZE_PUSH}], expected_channels=set()), # multiple policies
33
37
 
34
38
  # deny all and email
35
39
  PolicyCase(deny_values=[{ChannelType.EMAIL}, set(ChannelType)], expected_channels=set()),
@@ -0,0 +1,59 @@
1
+ """
2
+ Test cases for utils.braze
3
+ """
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from django.contrib.auth import get_user_model
9
+ from django.test import TestCase, override_settings
10
+
11
+ from edx_ace.utils.braze import get_braze_client
12
+
13
+ BRAZE_URL = "https://example.braze.com"
14
+ API_KEY = "test-api-key"
15
+ User = get_user_model()
16
+
17
+
18
+ @pytest.mark.django_db
19
+ class TestBrazeClient(TestCase):
20
+ """ Test cases for utils.braze """
21
+
22
+ @patch('edx_ace.utils.braze.BrazeClient')
23
+ def test_disabled(self, mock_braze_client):
24
+ """
25
+ Test that the channel is settings aren't configured.
26
+ """
27
+ result = get_braze_client()
28
+ self.assertEqual(result, None)
29
+ mock_braze_client.assert_not_called()
30
+
31
+ @override_settings(ACE_CHANNEL_BRAZE_API_KEY=API_KEY)
32
+ @patch('edx_ace.utils.braze.BrazeClient')
33
+ def test_braze_url_not_configured(self, mock_braze_client):
34
+ """
35
+ Test that the channel is settings aren't configured.
36
+ """
37
+ result = get_braze_client()
38
+ self.assertEqual(result, None)
39
+ mock_braze_client.assert_not_called()
40
+
41
+ @override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY)
42
+ @patch('edx_ace.utils.braze.BrazeClient')
43
+ def test_braze_api_key_not_configured(self, mock_braze_client):
44
+ """
45
+ Test that the channel is settings aren't configured.
46
+ """
47
+ result = get_braze_client()
48
+ self.assertEqual(result, None)
49
+ mock_braze_client.assert_not_called()
50
+
51
+ @override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY, ACE_CHANNEL_BRAZE_API_KEY=API_KEY)
52
+ @patch('edx_ace.utils.braze.BrazeClient', return_value=True)
53
+ def test_success(self, mock_braze_client):
54
+ """
55
+ Test that the channel is settings aren't configured.
56
+ """
57
+ result = get_braze_client()
58
+ self.assertEqual(result, True)
59
+ mock_braze_client.assert_called_once()
@@ -0,0 +1,27 @@
1
+ """
2
+ Helper Methods related to braze client
3
+ """
4
+
5
+ try:
6
+ from braze.client import BrazeClient
7
+ except ImportError:
8
+ BrazeClient = None
9
+ from django.conf import settings
10
+
11
+
12
+ def get_braze_client():
13
+ """ Returns a Braze client. """
14
+ if not BrazeClient:
15
+ return None
16
+
17
+ braze_api_key = getattr(settings, 'ACE_CHANNEL_BRAZE_API_KEY', None)
18
+ braze_api_url = getattr(settings, 'ACE_CHANNEL_BRAZE_REST_ENDPOINT', None)
19
+
20
+ if not braze_api_key or not braze_api_url:
21
+ return None
22
+
23
+ return BrazeClient(
24
+ api_key=braze_api_key,
25
+ api_url=f"https://{braze_api_url}",
26
+ app_id='',
27
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: edx-ace
3
- Version: 1.11.4
3
+ Version: 1.12.0
4
4
  Summary: Framework for Messaging
5
5
  Home-page: https://github.com/openedx/edx-ace
6
6
  Author: edX
@@ -27,6 +27,7 @@ edx_ace.egg-info/requires.txt
27
27
  edx_ace.egg-info/top_level.txt
28
28
  edx_ace/channel/__init__.py
29
29
  edx_ace/channel/braze.py
30
+ edx_ace/channel/braze_push_notification.py
30
31
  edx_ace/channel/django_email.py
31
32
  edx_ace/channel/file.py
32
33
  edx_ace/channel/mixins.py
@@ -42,6 +43,7 @@ edx_ace/tests/test_message.py
42
43
  edx_ace/tests/test_policy.py
43
44
  edx_ace/tests/test_presentation.py
44
45
  edx_ace/tests/channel/test_braze.py
46
+ edx_ace/tests/channel/test_braze_push_notification.py
45
47
  edx_ace/tests/channel/test_channel_helpers.py
46
48
  edx_ace/tests/channel/test_django_email.py
47
49
  edx_ace/tests/channel/test_file_email.py
@@ -49,8 +51,10 @@ edx_ace/tests/channel/test_push_notification.py
49
51
  edx_ace/tests/channel/test_sailthru.py
50
52
  edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/body.html
51
53
  edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/head.html
54
+ edx_ace/tests/utils/test_braze_utils.py
52
55
  edx_ace/tests/utils/test_signals.py
53
56
  edx_ace/utils/__init__.py
57
+ edx_ace/utils/braze.py
54
58
  edx_ace/utils/date.py
55
59
  edx_ace/utils/once.py
56
60
  edx_ace/utils/plugins.py
@@ -1,5 +1,6 @@
1
1
  [openedx.ace.channel]
2
2
  braze_email = edx_ace.channel.braze:BrazeEmailChannel
3
+ braze_push = edx_ace.channel.braze_push_notification:BrazePushNotificationChannel
3
4
  django_email = edx_ace.channel.django_email:DjangoEmailChannel
4
5
  file_email = edx_ace.channel.file:FileEmailChannel
5
6
  push_notification = edx_ace.channel.push_notification:PushNotificationChannel
@@ -1,6 +1,7 @@
1
1
  Django>=2.2
2
2
  attrs>=17.2.0
3
3
  django-push-notifications
4
+ edx-braze-client==1.0.1
4
5
  edx-django-utils>=5.14.2
5
6
  firebase-admin
6
7
  python-dateutil
@@ -5,6 +5,7 @@ Django>=2.2 # Web application framework
5
5
  python-dateutil # Python Date Utilities
6
6
  attrs>=17.2.0 # Attributes without boilerplate
7
7
  sailthru-client==2.2.3
8
+ edx-braze-client==1.0.1
8
9
  six
9
10
  stevedore>=1.10.0
10
11
  edx-django-utils>=5.14.2
@@ -30,20 +30,48 @@ def load_requirements(*requirements_paths):
30
30
  """
31
31
  # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why.
32
32
 
33
+ # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"}
34
+ by_canonical_name = {}
35
+
36
+ def check_name_consistent(package):
37
+ """
38
+ Raise exception if package is named different ways.
39
+
40
+ This ensures that packages are named consistently so we can match
41
+ constraints to packages. It also ensures that if we require a package
42
+ with extras we don't constrain it without mentioning the extras (since
43
+ that too would interfere with matching constraints.)
44
+ """
45
+ canonical = package.lower().replace('_', '-').split('[')[0]
46
+ seen_spelling = by_canonical_name.get(canonical)
47
+ if seen_spelling is None:
48
+ by_canonical_name[canonical] = package
49
+ elif seen_spelling != package:
50
+ raise Exception(
51
+ f'Encountered both "{seen_spelling}" and "{package}" in requirements '
52
+ 'and constraints files; please use just one or the other.'
53
+ )
54
+
33
55
  requirements = {}
34
56
  constraint_files = set()
35
57
 
36
- # groups "my-package-name<=x.y.z,..." into ("my-package-name", "<=x.y.z,...")
37
- requirement_line_regex = re.compile(r"([a-zA-Z0-9-_.]+)([<>=][^#\s]+)?")
58
+ # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...")
59
+ re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name
60
+ # Two groups: name[maybe,extras], and optionally a constraint
61
+ requirement_line_regex = re.compile(
62
+ r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?"
63
+ % (re_package_name_base_chars, re_package_name_base_chars)
64
+ )
38
65
 
39
66
  def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present):
40
67
  regex_match = requirement_line_regex.match(current_line)
41
68
  if regex_match:
42
69
  package = regex_match.group(1)
43
70
  version_constraints = regex_match.group(2)
71
+ check_name_consistent(package)
44
72
  existing_version_constraints = current_requirements.get(package, None)
45
- # it's fine to add constraints to an unconstrained package, but raise an error if there are already
46
- # constraints in place
73
+ # It's fine to add constraints to an unconstrained package,
74
+ # but raise an error if there are already constraints in place.
47
75
  if existing_version_constraints and existing_version_constraints != version_constraints:
48
76
  raise BaseException(f'Multiple constraint definitions found for {package}:'
49
77
  f' "{existing_version_constraints}" and "{version_constraints}".'
@@ -52,7 +80,8 @@ def load_requirements(*requirements_paths):
52
80
  if add_if_not_present or package in current_requirements:
53
81
  current_requirements[package] = version_constraints
54
82
 
55
- # process .in files and store the path to any constraint files that are pulled in
83
+ # Read requirements from .in files and store the path to any
84
+ # constraint files that are pulled in.
56
85
  for path in requirements_paths:
57
86
  with open(path) as reqs:
58
87
  for line in reqs:
@@ -61,7 +90,7 @@ def load_requirements(*requirements_paths):
61
90
  if line and line.startswith('-c') and not line.startswith('-c http'):
62
91
  constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip())
63
92
 
64
- # process constraint files and add any new constraints found to existing requirements
93
+ # process constraint files: add constraints to existing requirements
65
94
  for constraint_file in constraint_files:
66
95
  with open(constraint_file) as reader:
67
96
  for line in reader:
@@ -136,6 +165,7 @@ setup(
136
165
  'file_email = edx_ace.channel.file:FileEmailChannel',
137
166
  'django_email = edx_ace.channel.django_email:DjangoEmailChannel',
138
167
  'push_notification = edx_ace.channel.push_notification:PushNotificationChannel',
168
+ 'braze_push = edx_ace.channel.braze_push_notification:BrazePushNotificationChannel',
139
169
  ]
140
170
  }
141
171
  )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes