Skip to content

Commit

Permalink
feat(messages): handle dto and wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
ryshu committed Mar 2, 2023
1 parent ba45b62 commit f46aa7d
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 0 deletions.
2 changes: 2 additions & 0 deletions novu/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from novu.api.feed import FeedApi
from novu.api.integration import IntegrationApi
from novu.api.layout import LayoutApi
from novu.api.message import MessageApi
from novu.api.notification_group import NotificationGroupApi
from novu.api.notification_template import NotificationTemplateApi
from novu.api.subscriber import SubscriberApi
Expand All @@ -18,6 +19,7 @@
"FeedApi",
"IntegrationApi",
"LayoutApi",
"MessageApi",
"NotificationGroupApi",
"NotificationTemplateApi",
"SubscriberApi",
Expand Down
51 changes: 51 additions & 0 deletions novu/api/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
This module is used to define the ``MessageApi``, a python wrapper to interact with ``Messages`` in Novu.
"""
from typing import Dict, Optional, Union

from novu.api.base import Api
from novu.constants import MESSAGES_ENDPOINT
from novu.dto.message import PaginatedMessageDto


class MessageApi(Api):
"""This class aims to handle all API methods around messages in Novu"""

def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None) -> None:
super().__init__(url, api_key)

self._message_url = f"{self._url}{MESSAGES_ENDPOINT}"

def list(
self, limit: int = 10, page: int = 0, channel: Optional[str] = None, subscriber_id: Optional[str] = None
) -> PaginatedMessageDto:
"""List messages
Args:
limit: The number of messages to fetch, defaults to 10
page: The page to fetch, defaults to 0
channel: The channel for the messages you wish to list. Defaults to None.
subscriber_id: The subscriberId for the subscriber you like to list messages for
Returns:
Returned a paginated struct containing retrieved messages
"""
payload: Dict[str, Union[str, int]] = {"limit": limit, "page": page}

if channel:
payload["channel"] = channel
if subscriber_id:
payload["subscriberId"] = subscriber_id

return PaginatedMessageDto.from_camel_case(self.handle_request("GET", self._message_url, payload=payload))

def delete(self, message_id: str) -> bool:
"""Deletes a message entity from the Novu platform
Args:
message_id: The message ID to delete
Returns:
This function answer if the delete is a success by parsing the acknowledged field in response.
"""
return self.handle_request("DELETE", f"{self._message_url}/{message_id}")["data"].get("acknowledged", False)
1 change: 1 addition & 0 deletions novu/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
FEEDS_ENDPOINT = "/v1/feeds"
INTEGRATIONS_ENDPOINT = "/v1/integrations"
LAYOUTS_ENDPOINT = "/v1/layouts"
MESSAGES_ENDPOINT = "/v1/messages"
NOTIFICATION_GROUPS_ENDPOINT = "/v1/notification-groups"
NOTIFICATION_TEMPLATES_ENDPOINT = "/v1/notification-templates"
SUBSCRIBERS_ENDPOINT = "/v1/subscribers"
Expand Down
3 changes: 3 additions & 0 deletions novu/dto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from novu.dto.field import FieldFilterPartDto
from novu.dto.integration import IntegrationChannelUsageDto, IntegrationDto
from novu.dto.layout import LayoutDto, LayoutVariableDto, PaginatedLayoutDto
from novu.dto.message import MessageDto, PaginatedMessageDto
from novu.dto.notification_group import (
NotificationGroupDto,
PaginatedNotificationGroupDto,
Expand Down Expand Up @@ -44,6 +45,7 @@
"IntegrationDto",
"LayoutDto",
"LayoutVariableDto",
"MessageDto",
"NotificationGroupDto",
"NotificationStepDto",
"NotificationStepMetadataDto",
Expand All @@ -53,6 +55,7 @@
"NotificationTriggerVariableDto",
"PaginatedChangeDto",
"PaginatedLayoutDto",
"PaginatedMessageDto",
"PaginatedNotificationGroupDto",
"PaginatedNotificationTemplateDto",
"PaginatedSubscriberDto",
Expand Down
96 changes: 96 additions & 0 deletions novu/dto/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""This module is used to gather all DTO definitions related to the Message resource in Novu"""
import dataclasses
from typing import List, Optional

from novu.dto.base import CamelCaseDto, DtoIterableDescriptor
from novu.enums import Channel, ProviderIdEnum


@dataclasses.dataclass
class MessageDto(CamelCaseDto["MessageDto"]): # pylint: disable=R0902
"""Definition of an event"""

identifier: Optional[str] = None
"""The message identifier"""

_id: Optional[str] = None
"""Message Notification ID in Novu internal storage system"""

_template_id: Optional[str] = None
"""Template ID in Novu internal storage system"""

_environment_id: Optional[str] = None
"""Environment ID in Novu internal storage system"""

_message_template_id: Optional[str] = None
"""Message Template ID in Novu internal storage system"""

_organization_id: Optional[str] = None
"""Organization ID in Novu internal storage system"""

_subscriber_id: Optional[str] = None
"""Subscriber ID in Novu internal storage system"""

_job_id: Optional[str] = None
"""Job ID in Novu internal storage system"""

template_identifier: Optional[str] = None
"""Template ID in Novu internal storage system"""

email: Optional[str] = None
"""Email of the subscriber triggered where the message has been sent"""

subject: Optional[str] = None
"""Subject of the email sent"""

cta: Optional[dict] = None
"""Definition of the call to action used on message"""

channel: Optional[Channel] = None
"""Channel used for the message"""

content: Optional[str] = None
"""Content of the message"""

provider_id: Optional[ProviderIdEnum] = None
"""Provider ID used for the message"""

device_tokens: Optional[List[dict]] = None
"""A list of device tokens used on the provider to send message"""

seen: Optional[bool] = None
"""If the message has been seen."""

read: Optional[bool] = None
"""If the message has been read"""

status: Optional[str] = None
"""Status of the activity notification"""

transaction_id: Optional[str] = None
"""Transaction ID in Novu internal storage system"""

payload: Optional[dict] = None
"""Payload used during trigger"""

created_at: Optional[str] = None
updated_at: Optional[str] = None
deleted: Optional[bool] = None

last_read_date: Optional[str] = None
"""Timestamp of the last read event registered"""

last_seen_date: Optional[str] = None
"""Timestamp of the last seen event registered"""


@dataclasses.dataclass
class PaginatedMessageDto(CamelCaseDto["PaginatedMessageDto"]):
"""Paginated message definition"""

page: int = 0
total_count: int = 0
page_size: int = 0
data: DtoIterableDescriptor[MessageDto] = DtoIterableDescriptor[MessageDto](
default_factory=list, item_cls=MessageDto
)
121 changes: 121 additions & 0 deletions tests/api/test_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from unittest import TestCase, mock

from novu.api import MessageApi
from novu.config import NovuConfig
from novu.dto.message import MessageDto, PaginatedMessageDto
from novu.enums import Channel
from tests.factories import MockResponse


class MessageApiTests(TestCase):
@classmethod
def setUpClass(cls) -> None:
NovuConfig.configure("sample.novu.com", "api-key")
cls.api = MessageApi()

cls.response_json = {
"_id": "63daff4cc037e013fd82dadd",
"_templateId": "63daff36c037e013fd82da05",
"_environmentId": "63dafed97779f59258e38445",
"_messageTemplateId": "63daff36c037e013fd82d9f4",
"_notificationId": "63daff487779f59258e38b24",
"_organizationId": "63dafed97779f59258e3843f",
"_subscriberId": "63dafedbc037e013fd82d37a",
"_jobId": "63daff4c7779f59258e38b3c",
"templateIdentifier": "absences",
"cta": {"action": {"buttons": []}},
"_feedId": None,
"channel": "in_app",
"content": "test",
"deviceTokens": [],
"seen": True,
"read": True,
"status": "sent",
"transactionId": "aa287682-cb30-4a5f-a03a-f28f59c9d46d",
"deleted": False,
"createdAt": "2023-02-02T00:09:48.673Z",
"updatedAt": "2023-02-02T00:10:21.544Z",
"__v": 0,
"lastReadDate": "2023-02-02T00:10:21.544Z",
"lastSeenDate": "2023-02-02T00:10:21.544Z",
}
cls.maxDiff = None
cls.expected_dto = MessageDto(
identifier=None,
_id="63daff4cc037e013fd82dadd",
_template_id="63daff36c037e013fd82da05",
_environment_id="63dafed97779f59258e38445",
_message_template_id="63daff36c037e013fd82d9f4",
_organization_id="63dafed97779f59258e3843f",
_subscriber_id="63dafedbc037e013fd82d37a",
_job_id="63daff4c7779f59258e38b3c",
template_identifier="absences",
email=None,
subject=None,
cta={"action": {"buttons": []}},
channel="in_app",
content="test",
provider_id=None,
device_tokens=[],
seen=True,
read=True,
status="sent",
transaction_id="aa287682-cb30-4a5f-a03a-f28f59c9d46d",
payload=None,
created_at="2023-02-02T00:09:48.673Z",
updated_at="2023-02-02T00:10:21.544Z",
deleted=False,
last_read_date="2023-02-02T00:10:21.544Z",
last_seen_date="2023-02-02T00:10:21.544Z",
)

@mock.patch("requests.request")
def test_list_messages(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, {"data": [self.response_json]})

res = self.api.list()
self.assertIsInstance(res, PaginatedMessageDto)
self.assertEqual(list(res.data), [self.expected_dto])

mock_request.assert_called_once_with(
method="GET",
url="sample.novu.com/v1/messages",
headers={"Authorization": "ApiKey api-key"},
json=None,
params={"limit": 10, "page": 0},
timeout=5,
)

@mock.patch("requests.request")
def test_list_messages_with_filters(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, {"data": [self.response_json]})

res = self.api.list(10, 0, Channel.IN_APP.value, "63dafedbc037e013fd82d37a")
self.assertIsInstance(res, PaginatedMessageDto)
self.assertEqual(list(res.data), [self.expected_dto])

mock_request.assert_called_once_with(
method="GET",
url="sample.novu.com/v1/messages",
headers={"Authorization": "ApiKey api-key"},
json=None,
params={"limit": 10, "page": 0, "channel": "in_app", "subscriberId": "63dafedbc037e013fd82d37a"},
timeout=5,
)

@mock.patch("requests.request")
def test_delete_message(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, {"data": {"acknowledged": True, "status": "deleted"}})

res = self.api.delete("63e969fcb6729e21337e2360")
self.assertIsInstance(res, bool)
self.assertTrue(res)

mock_request.assert_called_once_with(
method="DELETE",
url="sample.novu.com/v1/messages/63e969fcb6729e21337e2360",
headers={"Authorization": "ApiKey api-key"},
json=None,
params=None,
timeout=5,
)

0 comments on commit f46aa7d

Please sign in to comment.