Skip to content

Commit da0edd7

Browse files
authored
MPT-14076 Add notifications batches (#87)
2 parents 73176ea + 8c8f0aa commit da0edd7

File tree

5 files changed

+257
-6
lines changed

5 files changed

+257
-6
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from httpx._types import FileTypes
2+
3+
from mpt_api_client.http import AsyncService, Service
4+
from mpt_api_client.http.mixins import _json_to_file_payload
5+
from mpt_api_client.models import FileModel, Model, ResourceData
6+
7+
8+
class Batch(Model):
9+
"""Notifications Batch resource."""
10+
11+
12+
class BatchesServiceConfig:
13+
"""Notifications Batches service configuration."""
14+
15+
_endpoint = "/public/v1/notifications/batches"
16+
_model_class = Batch
17+
_collection_key = "data"
18+
19+
20+
class BatchesService(
21+
Service[Batch],
22+
BatchesServiceConfig,
23+
):
24+
"""Notifications Batches service."""
25+
26+
def create(
27+
self,
28+
resource_data: ResourceData | None = None,
29+
files: dict[str, FileTypes] | None = None, # noqa: WPS221
30+
data_key: str = "_attachment_data",
31+
) -> Model:
32+
"""Create batch with attachments.
33+
34+
Args:
35+
resource_data: batch data.
36+
files: Files data.
37+
data_key: Key to use for the JSON data in the multipart form.
38+
39+
Returns:
40+
Created resource.
41+
"""
42+
files = files or {}
43+
44+
if resource_data:
45+
files[data_key] = (
46+
None,
47+
_json_to_file_payload(resource_data),
48+
"application/json",
49+
)
50+
51+
response = self.http_client.post(self.endpoint, files=files)
52+
response.raise_for_status()
53+
return self._model_class.from_response(response)
54+
55+
def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel:
56+
"""Get batch attachment.
57+
58+
Args:
59+
batch_id: Batch ID.
60+
attachment_id: Attachment ID.
61+
62+
Returns:
63+
FileModel containing the attachment.
64+
"""
65+
response = self.http_client.get(f"{self.endpoint}/{batch_id}/attachments/{attachment_id}")
66+
response.raise_for_status()
67+
return FileModel(response)
68+
69+
70+
class AsyncBatchesService(
71+
AsyncService[Batch],
72+
BatchesServiceConfig,
73+
):
74+
"""Async Notifications Batches service."""
75+
76+
async def create(
77+
self,
78+
resource_data: ResourceData | None = None,
79+
files: dict[str, FileTypes] | None = None, # noqa: WPS221
80+
data_key: str = "_attachment_data",
81+
) -> Model:
82+
"""Create batch with attachments.
83+
84+
Args:
85+
resource_data: batch data.
86+
files: Files data.
87+
data_key: Key to use for the JSON data in the multipart form.
88+
89+
Returns:
90+
Created resource.
91+
"""
92+
files = files or {}
93+
94+
if resource_data:
95+
files[data_key] = (
96+
None,
97+
_json_to_file_payload(resource_data),
98+
"application/json",
99+
)
100+
101+
response = await self.http_client.post(self.endpoint, files=files)
102+
response.raise_for_status()
103+
return self._model_class.from_response(response)
104+
105+
async def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel:
106+
"""Get batch attachment.
107+
108+
Args:
109+
batch_id: Batch ID.
110+
attachment_id: Attachment ID.
111+
112+
Returns:
113+
FileModel containing the attachment.
114+
"""
115+
response = await self.http_client.get(
116+
f"{self.endpoint}/{batch_id}/attachments/{attachment_id}"
117+
)
118+
response.raise_for_status()
119+
return FileModel(response)

mpt_api_client/resources/notifications/messages.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class MessagesService(
1818
Service[Message],
1919
MessagesServiceConfig,
2020
):
21-
"""Notifications Messages service (no CRUD, no block/unblock)."""
21+
"""Notifications Messages service."""
2222

2323

2424
class AsyncMessagesService(
2525
AsyncService[Message],
2626
MessagesServiceConfig,
2727
):
28-
"""Async Notifications Messages service (no CRUD, no block/unblock)."""
28+
"""Async Notifications Messages service."""

mpt_api_client/resources/notifications/notifications.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from mpt_api_client.http import AsyncHTTPClient, HTTPClient
2+
from mpt_api_client.resources.notifications.batches import AsyncBatchesService, BatchesService
23
from mpt_api_client.resources.notifications.categories import (
34
AsyncCategoriesService,
45
CategoriesService,
@@ -28,6 +29,11 @@ def messages(self) -> MessagesService:
2829
"""Messages service."""
2930
return MessagesService(http_client=self.http_client)
3031

32+
@property
33+
def batches(self) -> BatchesService:
34+
"""Batches service."""
35+
return BatchesService(http_client=self.http_client)
36+
3137

3238
class AsyncNotifications:
3339
"""Notifications MPT API Module."""
@@ -49,3 +55,8 @@ def contacts(self) -> AsyncContactsService:
4955
def messages(self) -> AsyncMessagesService:
5056
"""Async Messages service."""
5157
return AsyncMessagesService(http_client=self.http_client)
58+
59+
@property
60+
def batches(self) -> AsyncBatchesService:
61+
"""Async Batches service."""
62+
return AsyncBatchesService(http_client=self.http_client)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import io
2+
3+
import httpx
4+
import pytest
5+
import respx
6+
7+
from mpt_api_client.resources.notifications.batches import (
8+
AsyncBatchesService,
9+
BatchesService,
10+
)
11+
12+
13+
@pytest.fixture
14+
def batches_service(http_client):
15+
return BatchesService(http_client=http_client)
16+
17+
18+
@pytest.fixture
19+
def async_batches_service(async_http_client):
20+
return AsyncBatchesService(http_client=async_http_client)
21+
22+
23+
@pytest.mark.parametrize("method", ["get", "create", "iterate", "get_batch_attachment"])
24+
def test_sync_batches_service_methods(batches_service, method):
25+
assert hasattr(batches_service, method)
26+
27+
28+
@pytest.mark.parametrize("method", ["get", "create", "iterate", "get_batch_attachment"])
29+
def test_async_batches_service_methods(async_batches_service, method):
30+
assert hasattr(async_batches_service, method)
31+
32+
33+
def test_sync_get_batch_attachment(batches_service):
34+
attachment_content = b"Attachment file content or binary data"
35+
with respx.mock:
36+
mock_route = respx.get(
37+
"https://api.example.com/public/v1/notifications/batches/BAT-123/attachments/ATT-456"
38+
).mock(
39+
return_value=httpx.Response(
40+
status_code=httpx.codes.OK,
41+
headers={
42+
"content-type": "application/octet-stream",
43+
"content-disposition": (
44+
'form-data; name="file"; filename="batch_attachment.pdf"'
45+
),
46+
},
47+
content=attachment_content,
48+
)
49+
)
50+
downloaded_file = batches_service.get_batch_attachment("BAT-123", "ATT-456")
51+
52+
assert mock_route.call_count == 1
53+
assert downloaded_file.file_contents == attachment_content
54+
assert downloaded_file.content_type == "application/octet-stream"
55+
assert downloaded_file.filename == "batch_attachment.pdf"
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_async_get_batch_attachment(async_batches_service):
60+
attachment_content = b"Attachment file content or binary data"
61+
with respx.mock:
62+
mock_route = respx.get(
63+
"https://api.example.com/public/v1/notifications/batches/BAT-123/attachments/ATT-456"
64+
).mock(
65+
return_value=httpx.Response(
66+
status_code=httpx.codes.OK,
67+
headers={
68+
"content-type": "application/octet-stream",
69+
"content-disposition": (
70+
'form-data; name="file"; filename="batch_attachment.pdf"'
71+
),
72+
},
73+
content=attachment_content,
74+
)
75+
)
76+
downloaded_file = await async_batches_service.get_batch_attachment("BAT-123", "ATT-456")
77+
78+
assert mock_route.call_count == 1
79+
assert downloaded_file.file_contents == attachment_content
80+
assert downloaded_file.content_type == "application/octet-stream"
81+
assert downloaded_file.filename == "batch_attachment.pdf"
82+
83+
84+
def test_sync_batches_create_with_data(batches_service):
85+
batch_data = {"name": "Test Batch"}
86+
with respx.mock:
87+
mock_route = respx.post("https://api.example.com/public/v1/notifications/batches").mock(
88+
return_value=httpx.Response(
89+
status_code=httpx.codes.OK,
90+
json={"id": "BAT-133", "name": "Test Batch"},
91+
)
92+
)
93+
files = {"attachment": ("test.pdf", io.BytesIO(b"PDF content"), "application/pdf")}
94+
new_batch = batches_service.create(batch_data, files=files)
95+
request = mock_route.calls[0].request
96+
assert b'Content-Disposition: form-data; name="_attachment_data"' in request.content
97+
assert mock_route.call_count == 1
98+
assert new_batch.id == "BAT-133"
99+
assert new_batch.name == "Test Batch"
100+
101+
102+
@pytest.mark.asyncio
103+
async def test_async_batches_create_with_data(async_batches_service):
104+
batch_data = {"name": "Test Batch"}
105+
with respx.mock:
106+
mock_route = respx.post("https://api.example.com/public/v1/notifications/batches").mock(
107+
return_value=httpx.Response(
108+
status_code=httpx.codes.OK,
109+
json={"id": "BAT-133", "name": "Test Batch"},
110+
)
111+
)
112+
files = {"attachment": ("test.pdf", io.BytesIO(b"PDF content"), "application/pdf")}
113+
new_batch = await async_batches_service.create(batch_data, files=files)
114+
request = mock_route.calls[0].request
115+
assert b'Content-Disposition: form-data; name="_attachment_data"' in request.content
116+
assert mock_route.call_count == 1
117+
assert new_batch.id == "BAT-133"
118+
assert new_batch.name == "Test Batch"

tests/resources/notifications/test_notifications.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from mpt_api_client.resources import AsyncNotifications, Notifications
4+
from mpt_api_client.resources.notifications.batches import AsyncBatchesService, BatchesService
45
from mpt_api_client.resources.notifications.categories import (
56
AsyncCategoriesService,
67
CategoriesService,
@@ -29,12 +30,13 @@ def test_async_notifications_init(async_http_client):
2930
("categories", CategoriesService),
3031
("contacts", ContactsService),
3132
("messages", MessagesService),
33+
("batches", BatchesService),
3234
],
3335
)
3436
def test_notifications_properties(http_client, attr_name, expected):
35-
commerce = Notifications(http_client=http_client)
37+
notifications = Notifications(http_client=http_client)
3638

37-
service = getattr(commerce, attr_name)
39+
service = getattr(notifications, attr_name)
3840

3941
assert isinstance(service, expected)
4042

@@ -45,11 +47,12 @@ def test_notifications_properties(http_client, attr_name, expected):
4547
("categories", AsyncCategoriesService),
4648
("contacts", AsyncContactsService),
4749
("messages", AsyncMessagesService),
50+
("batches", AsyncBatchesService),
4851
],
4952
)
5053
def test_async_notifications_properties(http_client, attr_name, expected):
51-
commerce = AsyncNotifications(http_client=http_client)
54+
notifications = AsyncNotifications(http_client=http_client)
5255

53-
service = getattr(commerce, attr_name)
56+
service = getattr(notifications, attr_name)
5457

5558
assert isinstance(service, expected)

0 commit comments

Comments
 (0)