diff --git a/novu/api/event.py b/novu/api/event.py index de2983c8..48742dd2 100644 --- a/novu/api/event.py +++ b/novu/api/event.py @@ -8,7 +8,7 @@ from novu.api.base import Api from novu.constants import EVENTS_ENDPOINT -from novu.dto.event import EventDto, InputEventDto +from novu.dto.event import EventDto, InputEventDto, RecipientDto from novu.dto.topic import TriggerTopicDto @@ -29,7 +29,7 @@ def __init__( def trigger( self, name: str, - recipients: Union[str, List[str]], + recipients: Union[str, Iterable[str], RecipientDto, Iterable[RecipientDto]], payload: dict, overrides: Optional[dict] = None, transaction_id: Optional[str] = None, @@ -51,7 +51,14 @@ def trigger( Args: name: The name of the template trigger to activate. - recipients: A subscriber ID (or a list of subscriber ID) to reach with this trigger + recipients: + One of following format (to define the recipient of the event): + + * A subscriber ID + * A list of subscriber ID + * A SubscriberDto + * A list of SubscriberDto + payload: A JSON serializable python dict to pass additional custom information that could be used to render the template, or perform routing rules based on it. This data will also be available when fetching the @@ -72,7 +79,20 @@ def trigger( Returns: Create Event definition in Novu """ - payload = {"name": name, "to": recipients, "payload": payload} + tmp = [recipients] if not isinstance(recipients, Iterable) else recipients + + to: List[Union[str, dict]] = [] + for item in tmp: + if isinstance(item, str): + to.append(item) + elif isinstance(item, RecipientDto): + to.append(item.to_camel_case()) + + else: + raise ValueError( + f"Provided format for the 'recipients' arguments ({type(item)}) isn't supported by the SDK." + ) + payload = {"name": name, "to": to, "payload": payload} if overrides: payload["overrides"] = overrides if actor: @@ -161,7 +181,11 @@ def trigger_topic( """ _recipients = topics if isinstance(topics, Iterable) else [topics] - payload = {"name": name, "to": [r.to_camel_case() for r in _recipients], "payload": payload} + payload = { + "name": name, + "to": [r.to_camel_case() for r in _recipients], + "payload": payload, + } if overrides: payload["overrides"] = overrides if actor: diff --git a/novu/dto/__init__.py b/novu/dto/__init__.py index 600be966..e1d33d7f 100644 --- a/novu/dto/__init__.py +++ b/novu/dto/__init__.py @@ -11,7 +11,7 @@ EnvironmentDto, EnvironmentWidgetDto, ) -from novu.dto.event import EventDto +from novu.dto.event import EventDto, InputEventDto, RecipientDto from novu.dto.execution_detail import ExecutionDetailDto from novu.dto.feed import FeedDto from novu.dto.field import FieldFilterPartDto @@ -66,6 +66,8 @@ "EnvironmentDto", "EnvironmentWidgetDto", "EventDto", + "RecipientDto", + "InputEventDto", "ExecutionDetailDto", "FeedDto", "FieldFilterPartDto", diff --git a/novu/dto/event.py b/novu/dto/event.py index 2498d00f..02259718 100644 --- a/novu/dto/event.py +++ b/novu/dto/event.py @@ -2,7 +2,7 @@ import dataclasses from typing import List, Optional, Union -from novu.dto.base import CamelCaseDto +from novu.dto.base import CamelCaseDto, DtoDescriptor, DtoIterableDescriptor from novu.enums import EventStatus @@ -20,6 +20,35 @@ class EventDto(CamelCaseDto["EventDto"]): """Transaction id for trigger.""" +@dataclasses.dataclass +class RecipientDto(CamelCaseDto["RecipientDto"]): # pylint: disable=R0902 + """The recipients list of people who will receive the notification.""" + + subscriber_id: str + """Subscriber ID of the recipient.""" + + email: Optional[str] = None + """Email of the recipient.""" + + first_name: Optional[str] = None + """First name of the recipient.""" + + last_name: Optional[str] = None + """Last name of the recipient.""" + + phone: Optional[str] = None + """Phone number of the recipient.""" + + avatar: Optional[str] = None + """Avatar URL of the recipient.""" + + locale: Optional[str] = None + """Locale(language and region) of the recipient, .""" + + data: Optional[dict] = None + """Additional data for the recipient.""" + + @dataclasses.dataclass class InputEventDto(CamelCaseDto["InputEventDto"]): """Definition of an event used as an input""" @@ -27,12 +56,14 @@ class InputEventDto(CamelCaseDto["InputEventDto"]): name: str """The name of the template trigger to activate.""" - recipients: Union[str, List[str]] - """A subscriber ID (or a list of subscriber ID) to reach with this trigger.""" - payload: dict """A JSON serializable python dict to pass additional custom information.""" + recipients: DtoIterableDescriptor[RecipientDto] = DtoIterableDescriptor[RecipientDto]( + default_factory=list, item_cls=RecipientDto + ) + """A subscriber ID (or a list of subscriber ID) to reach with this trigger.""" + overrides: Optional[dict] = None """A JSON serializable python dict used to override provider specific configurations.""" diff --git a/tests/api/test_event.py b/tests/api/test_event.py index e6d8e3a6..7eb4042b 100644 --- a/tests/api/test_event.py +++ b/tests/api/test_event.py @@ -2,7 +2,7 @@ from novu.api import EventApi from novu.config import NovuConfig -from novu.dto.event import EventDto, InputEventDto +from novu.dto.event import EventDto, InputEventDto, RecipientDto from novu.dto.topic import TriggerTopicDto from novu.enums import EventStatus from tests.factories import MockResponse @@ -22,7 +22,7 @@ def test_trigger_with_single_recipient(self, mock_request: mock.MagicMock) -> No 201, {"data": {"acknowledged": True, "status": EventStatus.PROCESSED, "transactionId": "sample-test"}} ) - result = self.api.trigger("test-template", "sample-recipient", {}) + result = self.api.trigger("test-template", {"subscriber_id": "sample-recipient"}, {}) self.assertIsInstance(result, EventDto) self.assertTrue(result.acknowledged) @@ -32,7 +32,8 @@ def test_trigger_with_single_recipient(self, mock_request: mock.MagicMock) -> No method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": "sample-recipient", "payload": {}}, + # json={"name": "test-template", "to": {"subscriber_id": "sample-recipient"}, "payload": {}}, + json={"name": "test-template", "to": ["subscriber_id"], "payload": {}}, params=None, timeout=5, ) @@ -53,7 +54,11 @@ def test_trigger_with_multiple_recipients(self, mock_request: mock.MagicMock) -> method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": ["sample-recipient-1", "sample-recipient-2"], "payload": {}}, + json={ + "name": "test-template", + "to": ["sample-recipient-1", "sample-recipient-2"], + "payload": {}, + }, params=None, timeout=5, ) @@ -64,7 +69,7 @@ def test_trigger_with_overrides(self, mock_request: mock.MagicMock) -> None: 201, {"data": {"acknowledged": True, "status": EventStatus.PROCESSED, "transactionId": "sample-test"}} ) - result = self.api.trigger("test-template", "sample-recipient", {}, {"an": "override"}) + result = self.api.trigger("test-template", ["sample-recipient"], {}, {"an": "override"}) self.assertIsInstance(result, EventDto) self.assertTrue(result.acknowledged) @@ -74,7 +79,12 @@ def test_trigger_with_overrides(self, mock_request: mock.MagicMock) -> None: method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": "sample-recipient", "payload": {}, "overrides": {"an": "override"}}, + json={ + "name": "test-template", + "to": ["sample-recipient"], + "payload": {}, + "overrides": {"an": "override"}, + }, params=None, timeout=5, ) @@ -85,7 +95,7 @@ def test_trigger_with_actor(self, mock_request: mock.MagicMock) -> None: 201, {"data": {"acknowledged": True, "status": EventStatus.PROCESSED, "transactionId": "sample-test"}} ) - result = self.api.trigger("test-template", "sample-recipient", {}, actor="actor-id") + result = self.api.trigger("test-template", ["sample-recipient"], {}, actor="actor-id") self.assertIsInstance(result, EventDto) self.assertTrue(result.acknowledged) @@ -95,7 +105,12 @@ def test_trigger_with_actor(self, mock_request: mock.MagicMock) -> None: method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": "sample-recipient", "payload": {}, "actor": "actor-id"}, + json={ + "name": "test-template", + "to": ["sample-recipient"], + "payload": {}, + "actor": "actor-id", + }, params=None, timeout=5, ) @@ -106,7 +121,7 @@ def test_trigger_with_transaction_id(self, mock_request: mock.MagicMock) -> None 201, {"data": {"acknowledged": True, "status": EventStatus.PROCESSED, "transactionId": "sample-test"}} ) - result = self.api.trigger("test-template", "sample-recipient", {}, transaction_id="sample-test") + result = self.api.trigger("test-template", ["sample-recipient"], {}, transaction_id="sample-test") self.assertIsInstance(result, EventDto) self.assertTrue(result.acknowledged) @@ -116,7 +131,12 @@ def test_trigger_with_transaction_id(self, mock_request: mock.MagicMock) -> None method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": "sample-recipient", "payload": {}, "transactionId": "sample-test"}, + json={ + "name": "test-template", + "to": ["sample-recipient"], + "payload": {}, + "transactionId": "sample-test", + }, params=None, timeout=5, ) @@ -127,7 +147,7 @@ def test_trigger_with_tenant(self, mock_request: mock.MagicMock) -> None: 201, {"data": {"acknowledged": True, "status": EventStatus.PROCESSED, "transactionId": "sample-test"}} ) - result = self.api.trigger("test-template", "sample-recipient", {}, tenant="tenant-id") + result = self.api.trigger("test-template", ["sample-recipient"], {}, tenant="tenant-id") self.assertIsInstance(result, EventDto) self.assertTrue(result.acknowledged) @@ -137,7 +157,7 @@ def test_trigger_with_tenant(self, mock_request: mock.MagicMock) -> None: method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": "sample-recipient", "payload": {}, "tenant": "tenant-id"}, + json={"name": "test-template", "to": ["sample-recipient"], "payload": {}, "tenant": "tenant-id"}, params=None, timeout=5, ) @@ -159,7 +179,11 @@ def test_trigger_topic_with_single_topic(self, mock_request: mock.MagicMock) -> method="POST", url="sample.novu.com/v1/events/trigger", headers={"Authorization": "ApiKey api-key"}, - json={"name": "test-template", "to": [{"topicKey": "topic-key", "type": "type"}], "payload": {}}, + json={ + "name": "test-template", + "to": [{"topicKey": "topic-key", "type": "type"}], + "payload": {}, + }, params=None, timeout=5, ) @@ -311,7 +335,7 @@ def test_trigger_bulk_with_one_event(self, mock_request: mock.MagicMock) -> None ) events = [ - InputEventDto(name="test-template", recipients="recipient_1", payload={}), + InputEventDto(name="test-template", recipients=["recipient_1"], payload={}), ] result = self.api.trigger_bulk(events) @@ -329,7 +353,7 @@ def test_trigger_bulk_with_one_event(self, mock_request: mock.MagicMock) -> None headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}}, + {"name": "test-template", "to": ["recipient_1"], "payload": {}}, ] }, params=None, @@ -351,10 +375,9 @@ def test_trigger_bulk_with_with_multiple(self, mock_request: mock.MagicMock) -> ) events = [ - InputEventDto(name="test-template", recipients="recipient_1", payload={}), - InputEventDto(name="test-template", recipients="recipient_2", payload={}), + InputEventDto(name="test-template", recipients=["recipient_1"], payload={}), + InputEventDto(name="test-template", recipients=["recipient_2"], payload={}), ] - result = self.api.trigger_bulk(events) self.assertEqual(len(result), len(events)) @@ -371,8 +394,8 @@ def test_trigger_bulk_with_with_multiple(self, mock_request: mock.MagicMock) -> headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}}, - {"name": "test-template", "to": "recipient_2", "payload": {}}, + {"name": "test-template", "to": ["recipient_1"], "payload": {}}, + {"name": "test-template", "to": ["recipient_2"], "payload": {}}, ] }, params=None, @@ -394,8 +417,8 @@ def test_trigger_bulk_with_multiple_events_and_overrides(self, mock_request: moc ) events = [ - InputEventDto(name="test-template", recipients="recipient_1", payload={}, overrides={"an": "override"}), - InputEventDto(name="test-template", recipients="recipient_2", payload={}), + InputEventDto(name="test-template", recipients=["recipient_1"], payload={}, overrides={"an": "override"}), + InputEventDto(name="test-template", recipients=["recipient_2"], payload={}), ] result = self.api.trigger_bulk(events) @@ -414,8 +437,8 @@ def test_trigger_bulk_with_multiple_events_and_overrides(self, mock_request: moc headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}, "overrides": {"an": "override"}}, - {"name": "test-template", "to": "recipient_2", "payload": {}}, + {"name": "test-template", "to": ["recipient_1"], "payload": {}, "overrides": {"an": "override"}}, + {"name": "test-template", "to": ["recipient_2"], "payload": {}}, ] }, params=None, @@ -437,10 +460,9 @@ def test_trigger_bulk_with_multiple_events_and_actor(self, mock_request: mock.Ma ) events = [ - InputEventDto(name="test-template", recipients="recipient_1", payload={}, actor="actor-id"), - InputEventDto(name="test-template", recipients="recipient_2", payload={}), + InputEventDto(name="test-template", recipients=["recipient_1"], payload={}, actor="actor-id"), + InputEventDto(name="test-template", recipients=["recipient_2"], payload={}), ] - result = self.api.trigger_bulk(events) self.assertEqual(len(result), len(events)) @@ -457,8 +479,8 @@ def test_trigger_bulk_with_multiple_events_and_actor(self, mock_request: mock.Ma headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}, "actor": "actor-id"}, - {"name": "test-template", "to": "recipient_2", "payload": {}}, + {"name": "test-template", "to": ["recipient_1"], "payload": {}, "actor": "actor-id"}, + {"name": "test-template", "to": ["recipient_2"], "payload": {}}, ] }, params=None, @@ -481,9 +503,9 @@ def test_trigger_bulk_with_multiple_events_and_transaction_id(self, mock_request events = [ InputEventDto( - name="test-template", recipients="recipient_1", payload={}, transaction_id=transaction_ids[0] + name="test-template", recipients=["recipient_1"], payload={}, transaction_id=transaction_ids[0] ), - InputEventDto(name="test-template", recipients="recipient_2", payload={}), + InputEventDto(name="test-template", recipients=["recipient_2"], payload={}), ] result = self.api.trigger_bulk(events) @@ -502,8 +524,13 @@ def test_trigger_bulk_with_multiple_events_and_transaction_id(self, mock_request headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}, "transactionId": transaction_ids[0]}, - {"name": "test-template", "to": "recipient_2", "payload": {}}, + { + "name": "test-template", + "to": ["recipient_1"], + "payload": {}, + "transactionId": transaction_ids[0], + }, + {"name": "test-template", "to": ["recipient_2"], "payload": {}}, ] }, params=None, @@ -525,8 +552,8 @@ def test_trigger_bulk_with_multiple_events_and_tenant(self, mock_request: mock.M ) events = [ - InputEventDto(name="test-template", recipients="recipient_1", payload={}, tenant="tenant-id"), - InputEventDto(name="test-template", recipients="recipient_2", payload={}), + InputEventDto(name="test-template", recipients=["recipient_1"], payload={}, tenant="tenant-id"), + InputEventDto(name="test-template", recipients=["recipient_2"], payload={}), ] result = self.api.trigger_bulk(events) @@ -545,8 +572,8 @@ def test_trigger_bulk_with_multiple_events_and_tenant(self, mock_request: mock.M headers={"Authorization": "ApiKey api-key"}, json={ "events": [ - {"name": "test-template", "to": "recipient_1", "payload": {}, "tenant": "tenant-id"}, - {"name": "test-template", "to": "recipient_2", "payload": {}}, + {"name": "test-template", "to": ["recipient_1"], "payload": {}, "tenant": "tenant-id"}, + {"name": "test-template", "to": ["recipient_2"], "payload": {}}, ] }, params=None,