Repository: Stranger6667/postmarker

Issue:
Add support for batchWithTemplates
https://postmarkapp.com/developer/api/templates-api#send-batch-with-templates

Discussion:

Patch:
diff --git a/docs/emails.rst b/docs/emails.rst
index b0b7dc3..22fb0d7 100644
--- a/docs/emails.rst
+++ b/docs/emails.rst
@@ -202,3 +202,12 @@ Batches are available via the :py:meth:`~postmarker.models.emails.EmailManager.E
 
 For now, batches expose a very limited interface - only :py:meth:`~postmarker.models.emails.EmailBatch.send` method and
 length information via the ``len`` function.
+
+Template batches are available via the :py:meth:`~postmarker.models.emails.EmailManager.EmailTemplateBatch` constructor.
+
+.. code-block:: python
+
+    >>> batch = postmark.emails.EmailTemplateBatch(template)
+    >>> len(batch)
+    1
+    >>> batch.send()
diff --git a/src/postmarker/models/emails.py b/src/postmarker/models/emails.py
index f2f2313..649767f 100644
--- a/src/postmarker/models/emails.py
+++ b/src/postmarker/models/emails.py
@@ -223,6 +223,45 @@ class EmailTemplate(BaseEmail):
         return self._manager._send_with_template(**self.as_dict())
 
 
+class EmailTemplateBatch(Model):
+    """Gathers multiple email templates in a single batch."""
+
+    MAX_SIZE = 500
+
+    def __init__(self, *emails, **kwargs):
+        self.emails = emails
+        super().__init__(**kwargs)
+
+    def __len__(self):
+        return len(self.emails)
+
+    def as_dict(self, **extra):
+        """Converts all available emails to dictionaries.
+
+        :return: List of dictionaries.
+        """
+        return [self._construct_email(email, **extra) for email in self.emails]
+
+    def _construct_email(self, email, **extra):
+        """Converts incoming data to properly structured dictionary."""
+        if isinstance(email, dict):
+            email = EmailTemplate(manager=self._manager, **email)
+        elif not isinstance(email, EmailTemplate):
+            raise ValueError
+        email._update(extra)
+        return email.as_dict()
+
+    def send(self, **extra):
+        """Sends email batch.
+
+        :return: Information about sent emails.
+        :rtype: `list`
+        """
+        emails = self.as_dict(**extra)
+        responses = [self._manager._send_batch_with_template(*batch) for batch in chunks(emails, self.MAX_SIZE)]
+        return sum(responses, [])
+
+
 class EmailBatch(Model):
     """Gathers multiple emails in a single batch."""
 
@@ -281,6 +320,9 @@ class EmailManager(ModelManager):
     def _send_with_template(self, **kwargs):
         return self.call("POST", "/email/withTemplate/", data=kwargs)
 
+    def _send_batch_with_template(self, *email_templates):
+        return self.call("POST", "/email/batchWithTemplates/", data={"Messages": email_templates})
+
     def _send_batch(self, *emails):
         """Low-level batch send call."""
         return self.call("POST", "/email/batch", data=emails)
@@ -404,6 +446,14 @@ class EmailManager(ModelManager):
         """
         return self.EmailBatch(*emails).send(**extra)
 
+    def send_template_batch(self, *emails, **extra):
+        """Sends an email batch.
+
+        :param emails: :py:class:`TemplateEmail` instances or dictionaries
+        :param extra: dictionary with extra arguments for every message in the batch.
+        """
+        return self.EmailTemplateBatch(*emails).send(**extra)
+
     # NOTE. The following methods are included here to expose better interface without need to import relevant classes.
 
     def Email(
@@ -499,3 +549,10 @@ class EmailManager(ModelManager):
         :return: :py:class:`EmailBatch`
         """
         return EmailBatch(*emails, manager=self)
+
+    def EmailTemplateBatch(self, *emails):
+        """Constructs :py:class:`EmailTemplateBatch` instance.
+
+        :return: :py:class:`EmailTemplateBatch`
+        """
+        return EmailTemplateBatch(*emails, manager=self)


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index 89422ad..2d2f0ea 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -87,11 +87,23 @@ def email(postmark):
     return postmark.emails.Email(From="sender@example.com", To="receiver@example.com", TextBody="text")
 
 
+@pytest.fixture()
+def email_template(postmark):
+    return postmark.emails.EmailTemplate(
+        From="sender@example.com", To="receiver@example.com", TemplateId=983381, TemplateModel={}
+    )
+
+
 @pytest.fixture
 def email_batch(postmark, email):
     return postmark.emails.EmailBatch(email)
 
 
+@pytest.fixture
+def email_template_batch(postmark, email):
+    return postmark.emails.EmailTemplateBatch(email)
+
+
 @pytest.fixture(scope="session")
 def template(postmark):
     return postmark.templates.get(983381)
diff --git a/test/models/test_emails.py b/test/models/test_emails.py
index eaac287..4cc5dde 100644
--- a/test/models/test_emails.py
+++ b/test/models/test_emails.py
@@ -377,3 +377,45 @@ class TestEmail:
 class TestDelivery:
     def test_str(self, delivery_webhook):
         assert str(delivery_webhook) == "Delivery to john@example.com"
+
+
+class TestTemplateBatchSend:
+    def test_template_email_instance(self, postmark, email_template, postmark_request):
+        postmark.emails.send_template_batch(email_template)
+        assert postmark_request.call_args[1]["json"] == {"Messages": (email_template.as_dict(),)}
+
+    def test_dict(self, postmark, postmark_request):
+        template_dict = {
+            "TemplateId": 983381,
+            "TemplateModel": {},
+            "From": "sender@example.com",
+            "To": "receiver@example.com",
+        }
+        expected = {
+            "TemplateId": 983381,
+            "TemplateModel": {},
+            "From": "sender@example.com",
+            "To": "receiver@example.com",
+            "Headers": [],
+            "Attachments": [],
+        }
+        postmark.emails.send_template_batch(template_dict)
+        assert postmark_request.call_args[1]["json"] == {"Messages": (expected,)}
+
+    def test_multiple(self, postmark, email_template, postmark_request):
+        postmark.emails.send_template_batch(email_template, email_template)
+        assert postmark_request.call_args[1]["json"] == {
+            "Messages": (
+                email_template.as_dict(),
+                email_template.as_dict(),
+            )
+        }
+
+    def test_invalid(self, postmark):
+        with pytest.raises(ValueError):
+            postmark.emails.send_template_batch(object())
+
+
+class TestEmailTemplateBatch:
+    def test_len(self, email_template_batch):
+        assert len(email_template_batch) == 1