diff --git a/mpt_api_client/resources/notifications/batches.py b/mpt_api_client/resources/notifications/batches.py new file mode 100644 index 00000000..2eb76a2f --- /dev/null +++ b/mpt_api_client/resources/notifications/batches.py @@ -0,0 +1,119 @@ +from httpx._types import FileTypes + +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import _json_to_file_payload +from mpt_api_client.models import FileModel, Model, ResourceData + + +class Batch(Model): + """Notifications Batch resource.""" + + +class BatchesServiceConfig: + """Notifications Batches service configuration.""" + + _endpoint = "/public/v1/notifications/batches" + _model_class = Batch + _collection_key = "data" + + +class BatchesService( + Service[Batch], + BatchesServiceConfig, +): + """Notifications Batches service.""" + + def create( + self, + resource_data: ResourceData | None = None, + files: dict[str, FileTypes] | None = None, # noqa: WPS221 + data_key: str = "_attachment_data", + ) -> Model: + """Create batch with attachments. + + Args: + resource_data: batch data. + files: Files data. + data_key: Key to use for the JSON data in the multipart form. + + Returns: + Created resource. + """ + files = files or {} + + if resource_data: + files[data_key] = ( + None, + _json_to_file_payload(resource_data), + "application/json", + ) + + response = self.http_client.post(self.endpoint, files=files) + response.raise_for_status() + return self._model_class.from_response(response) + + def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel: + """Get batch attachment. + + Args: + batch_id: Batch ID. + attachment_id: Attachment ID. + + Returns: + FileModel containing the attachment. + """ + response = self.http_client.get(f"{self.endpoint}/{batch_id}/attachments/{attachment_id}") + response.raise_for_status() + return FileModel(response) + + +class AsyncBatchesService( + AsyncService[Batch], + BatchesServiceConfig, +): + """Async Notifications Batches service.""" + + async def create( + self, + resource_data: ResourceData | None = None, + files: dict[str, FileTypes] | None = None, # noqa: WPS221 + data_key: str = "_attachment_data", + ) -> Model: + """Create batch with attachments. + + Args: + resource_data: batch data. + files: Files data. + data_key: Key to use for the JSON data in the multipart form. + + Returns: + Created resource. + """ + files = files or {} + + if resource_data: + files[data_key] = ( + None, + _json_to_file_payload(resource_data), + "application/json", + ) + + response = await self.http_client.post(self.endpoint, files=files) + response.raise_for_status() + return self._model_class.from_response(response) + + async def get_batch_attachment(self, batch_id: str, attachment_id: str) -> FileModel: + """Get batch attachment. + + Args: + batch_id: Batch ID. + attachment_id: Attachment ID. + + Returns: + FileModel containing the attachment. + """ + response = await self.http_client.get( + f"{self.endpoint}/{batch_id}/attachments/{attachment_id}" + ) + response.raise_for_status() + return FileModel(response) diff --git a/mpt_api_client/resources/notifications/messages.py b/mpt_api_client/resources/notifications/messages.py index c74343b3..860e881c 100644 --- a/mpt_api_client/resources/notifications/messages.py +++ b/mpt_api_client/resources/notifications/messages.py @@ -18,11 +18,11 @@ class MessagesService( Service[Message], MessagesServiceConfig, ): - """Notifications Messages service (no CRUD, no block/unblock).""" + """Notifications Messages service.""" class AsyncMessagesService( AsyncService[Message], MessagesServiceConfig, ): - """Async Notifications Messages service (no CRUD, no block/unblock).""" + """Async Notifications Messages service.""" diff --git a/mpt_api_client/resources/notifications/notifications.py b/mpt_api_client/resources/notifications/notifications.py index 22a33802..cc4a1cde 100644 --- a/mpt_api_client/resources/notifications/notifications.py +++ b/mpt_api_client/resources/notifications/notifications.py @@ -1,4 +1,5 @@ from mpt_api_client.http import AsyncHTTPClient, HTTPClient +from mpt_api_client.resources.notifications.batches import AsyncBatchesService, BatchesService from mpt_api_client.resources.notifications.categories import ( AsyncCategoriesService, CategoriesService, @@ -28,6 +29,11 @@ def messages(self) -> MessagesService: """Messages service.""" return MessagesService(http_client=self.http_client) + @property + def batches(self) -> BatchesService: + """Batches service.""" + return BatchesService(http_client=self.http_client) + class AsyncNotifications: """Notifications MPT API Module.""" @@ -49,3 +55,8 @@ def contacts(self) -> AsyncContactsService: def messages(self) -> AsyncMessagesService: """Async Messages service.""" return AsyncMessagesService(http_client=self.http_client) + + @property + def batches(self) -> AsyncBatchesService: + """Async Batches service.""" + return AsyncBatchesService(http_client=self.http_client) diff --git a/tests/resources/notifications/test_batches.py b/tests/resources/notifications/test_batches.py new file mode 100644 index 00000000..810376dd --- /dev/null +++ b/tests/resources/notifications/test_batches.py @@ -0,0 +1,118 @@ +import io + +import httpx +import pytest +import respx + +from mpt_api_client.resources.notifications.batches import ( + AsyncBatchesService, + BatchesService, +) + + +@pytest.fixture +def batches_service(http_client): + return BatchesService(http_client=http_client) + + +@pytest.fixture +def async_batches_service(async_http_client): + return AsyncBatchesService(http_client=async_http_client) + + +@pytest.mark.parametrize("method", ["get", "create", "iterate", "get_batch_attachment"]) +def test_sync_batches_service_methods(batches_service, method): + assert hasattr(batches_service, method) + + +@pytest.mark.parametrize("method", ["get", "create", "iterate", "get_batch_attachment"]) +def test_async_batches_service_methods(async_batches_service, method): + assert hasattr(async_batches_service, method) + + +def test_sync_get_batch_attachment(batches_service): + attachment_content = b"Attachment file content or binary data" + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/notifications/batches/BAT-123/attachments/ATT-456" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={ + "content-type": "application/octet-stream", + "content-disposition": ( + 'form-data; name="file"; filename="batch_attachment.pdf"' + ), + }, + content=attachment_content, + ) + ) + downloaded_file = batches_service.get_batch_attachment("BAT-123", "ATT-456") + + assert mock_route.call_count == 1 + assert downloaded_file.file_contents == attachment_content + assert downloaded_file.content_type == "application/octet-stream" + assert downloaded_file.filename == "batch_attachment.pdf" + + +@pytest.mark.asyncio +async def test_async_get_batch_attachment(async_batches_service): + attachment_content = b"Attachment file content or binary data" + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/notifications/batches/BAT-123/attachments/ATT-456" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={ + "content-type": "application/octet-stream", + "content-disposition": ( + 'form-data; name="file"; filename="batch_attachment.pdf"' + ), + }, + content=attachment_content, + ) + ) + downloaded_file = await async_batches_service.get_batch_attachment("BAT-123", "ATT-456") + + assert mock_route.call_count == 1 + assert downloaded_file.file_contents == attachment_content + assert downloaded_file.content_type == "application/octet-stream" + assert downloaded_file.filename == "batch_attachment.pdf" + + +def test_sync_batches_create_with_data(batches_service): + batch_data = {"name": "Test Batch"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/notifications/batches").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json={"id": "BAT-133", "name": "Test Batch"}, + ) + ) + files = {"attachment": ("test.pdf", io.BytesIO(b"PDF content"), "application/pdf")} + new_batch = batches_service.create(batch_data, files=files) + request = mock_route.calls[0].request + assert b'Content-Disposition: form-data; name="_attachment_data"' in request.content + assert mock_route.call_count == 1 + assert new_batch.id == "BAT-133" + assert new_batch.name == "Test Batch" + + +@pytest.mark.asyncio +async def test_async_batches_create_with_data(async_batches_service): + batch_data = {"name": "Test Batch"} + with respx.mock: + mock_route = respx.post("https://api.example.com/public/v1/notifications/batches").mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + json={"id": "BAT-133", "name": "Test Batch"}, + ) + ) + files = {"attachment": ("test.pdf", io.BytesIO(b"PDF content"), "application/pdf")} + new_batch = await async_batches_service.create(batch_data, files=files) + request = mock_route.calls[0].request + assert b'Content-Disposition: form-data; name="_attachment_data"' in request.content + assert mock_route.call_count == 1 + assert new_batch.id == "BAT-133" + assert new_batch.name == "Test Batch" diff --git a/tests/resources/notifications/test_notifications.py b/tests/resources/notifications/test_notifications.py index f06aabf0..7f22eea5 100644 --- a/tests/resources/notifications/test_notifications.py +++ b/tests/resources/notifications/test_notifications.py @@ -1,6 +1,7 @@ import pytest from mpt_api_client.resources import AsyncNotifications, Notifications +from mpt_api_client.resources.notifications.batches import AsyncBatchesService, BatchesService from mpt_api_client.resources.notifications.categories import ( AsyncCategoriesService, CategoriesService, @@ -29,12 +30,13 @@ def test_async_notifications_init(async_http_client): ("categories", CategoriesService), ("contacts", ContactsService), ("messages", MessagesService), + ("batches", BatchesService), ], ) def test_notifications_properties(http_client, attr_name, expected): - commerce = Notifications(http_client=http_client) + notifications = Notifications(http_client=http_client) - service = getattr(commerce, attr_name) + service = getattr(notifications, attr_name) assert isinstance(service, expected) @@ -45,11 +47,12 @@ def test_notifications_properties(http_client, attr_name, expected): ("categories", AsyncCategoriesService), ("contacts", AsyncContactsService), ("messages", AsyncMessagesService), + ("batches", AsyncBatchesService), ], ) def test_async_notifications_properties(http_client, attr_name, expected): - commerce = AsyncNotifications(http_client=http_client) + notifications = AsyncNotifications(http_client=http_client) - service = getattr(commerce, attr_name) + service = getattr(notifications, attr_name) assert isinstance(service, expected)