From 8af39753aa68122a9cca7f4ece881096543d994d Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Fri, 12 Jun 2020 16:08:22 -0500 Subject: [PATCH] feat: add support for dynamic template data to Email class Fixes https://github.com/sendgrid/sendgrid-python/issues/899 This is helpful when sending multiple emails to multiple recipients. You can now include the dynamic template data with the recipient which will then be included in the personalization. --- .../helpers/mail/dynamic_template_data.py | 12 +-- sendgrid/helpers/mail/email.py | 49 +++++++---- sendgrid/helpers/mail/mail.py | 2 +- sendgrid/helpers/mail/personalization.py | 20 +++-- test/test_mail_helpers.py | 82 +++++++++++++++---- ..._multiple_emails_to_multiple_recipients.md | 23 +++--- 6 files changed, 129 insertions(+), 59 deletions(-) diff --git a/sendgrid/helpers/mail/dynamic_template_data.py b/sendgrid/helpers/mail/dynamic_template_data.py index d682dbf2d..e12967b70 100644 --- a/sendgrid/helpers/mail/dynamic_template_data.py +++ b/sendgrid/helpers/mail/dynamic_template_data.py @@ -5,10 +5,10 @@ class DynamicTemplateData(object): def __init__(self, dynamic_template_data=None, p=0): """Data for a transactional template. - Should be JSON-serializeable structure. + Should be JSON-serializable structure. :param dynamic_template_data: Data for a transactional template. - :type dynamic_template_data: A JSON-serializeable structure + :type dynamic_template_data: A JSON-serializable structure :param name: p is the Personalization object or Personalization object index :type name: Personalization, integer, optional @@ -25,7 +25,7 @@ def __init__(self, dynamic_template_data=None, p=0): def dynamic_template_data(self): """Data for a transactional template. - :rtype: A JSON-serializeable structure + :rtype: A JSON-serializable structure """ return self._dynamic_template_data @@ -34,7 +34,7 @@ def dynamic_template_data(self, value): """Data for a transactional template. :param value: Data for a transactional template. - :type value: A JSON-serializeable structure + :type value: A JSON-serializable structure """ self._dynamic_template_data = value @@ -59,7 +59,7 @@ def personalization(self, value): def __str__(self): """Get a JSON representation of this object. - :rtype: A JSON-serializeable structure + :rtype: A JSON-serializable structure """ return str(self.get()) @@ -68,6 +68,6 @@ def get(self): Get a JSON-ready representation of this DynamicTemplateData object. :returns: Data for a transactional template. - :rtype: A JSON-serializeable structure. + :rtype: A JSON-serializable structure. """ return self.dynamic_template_data diff --git a/sendgrid/helpers/mail/email.py b/sendgrid/helpers/mail/email.py index 3c2cb3bbb..ba3a98848 100644 --- a/sendgrid/helpers/mail/email.py +++ b/sendgrid/helpers/mail/email.py @@ -18,7 +18,7 @@ html_entity_decode = __html_parser__.unescape try: - basestring = basestring + basestring = basestring except NameError: # Define basestring when Python >= 3.0 basestring = str @@ -32,7 +32,8 @@ def __init__(self, name=None, substitutions=None, subject=None, - p=0): + p=0, + dynamic_template_data=None): """Create an Email with the given address and name. Either fill the separate name and email fields, or pass all information @@ -41,17 +42,19 @@ def __init__(self, :type email: string, optional :param name: Name for this sender or recipient. :type name: string, optional + :param substitutions: String substitutions to be applied to the email. + :type substitutions: list(Substitution), optional :param subject: Subject for this sender or recipient. :type subject: string, optional :param p: p is the Personalization object or Personalization object index :type p: Personalization, integer, optional + :param dynamic_template_data: Data for a dynamic transactional template. + :type dynamic_template_data: DynamicTemplateData, optional """ self._name = None self._email = None - self._substitutions = None - self._subject = None - self._personalization = None + self._personalization = p if email and not name: # allows passing emails as "Example Name " @@ -64,14 +67,11 @@ def __init__(self, if name is not None: self.name = name - if substitutions is not None: - self.substitutions = substitutions - - if subject is not None: - self.subject = subject - - if p is not None: - self.personalization = p + # Note that these only apply to To Emails (see Personalization.add_to) + # and should be moved but have not been for compatibility. + self._substitutions = substitutions + self._dynamic_template_data = dynamic_template_data + self._subject = subject @property def name(self): @@ -129,7 +129,7 @@ def email(self, value): @property def substitutions(self): """A list of Substitution objects. These substitutions will apply to - the text and html content of the body of your email, in addition + the text and html content of the body of your email, in addition to the subject and reply-to parameters. The total collective size of your substitutions may not exceed 10,000 bytes per personalization object. @@ -141,13 +141,13 @@ def substitutions(self): @substitutions.setter def substitutions(self, value): """A list of Substitution objects. These substitutions will apply to - the text and html content of the body of your email, in addition to + the text and html content of the body of your email, in addition to the subject and reply-to parameters. The total collective size of your substitutions may not exceed 10,000 bytes per personalization object. :param value: A list of Substitution objects. These substitutions will - apply to the text and html content of the body of your email, in + apply to the text and html content of the body of your email, in addition to the subject and reply-to parameters. The total collective size of your substitutions may not exceed 10,000 bytes per personalization object. @@ -155,6 +155,23 @@ def substitutions(self, value): """ self._substitutions = value + @property + def dynamic_template_data(self): + """Data for a dynamic transactional template. + + :rtype: DynamicTemplateData + """ + return self._dynamic_template_data + + @dynamic_template_data.setter + def dynamic_template_data(self, value): + """Data for a dynamic transactional template. + + :param value: DynamicTemplateData + :type value: DynamicTemplateData + """ + self._dynamic_template_data = value + @property def subject(self): """Subject for this sender or recipient. diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 5ac3ef800..6c8e004b6 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -627,7 +627,7 @@ def dynamic_template_data(self, value): """Data for a transactional template :param value: Data for a transactional template - :type value: DynamicTemplateData, a JSON-serializeable structure + :type value: DynamicTemplateData, a JSON-serializable structure """ if not isinstance(value, DynamicTemplateData): value = DynamicTemplateData(value) diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 835933017..9239f9458 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -51,11 +51,16 @@ def add_to(self, email): self.add_substitution(substitution) else: self.add_substitution(email.substitutions) + + if email.dynamic_template_data: + self.dynamic_template_data = email.dynamic_template_data + if email.subject: if isinstance(email.subject, str): self.subject = email.subject else: self.subject = email.subject.get() + self._tos.append(email.get()) @property @@ -149,10 +154,10 @@ def add_substitution(self, substitution): :type substitution: Substitution """ - if isinstance(substitution, dict): - self._substitutions.append(substitution) - else: - self._substitutions.append(substitution.get()) + if not isinstance(substitution, dict): + substitution = substitution.get() + + self._substitutions.append(substitution) @property def custom_args(self): @@ -190,14 +195,17 @@ def send_at(self, value): @property def dynamic_template_data(self): """Data for dynamic transactional template. - Should be JSON-serializeable structure. + Should be JSON-serializable structure. - :rtype: JSON-serializeable structure + :rtype: JSON-serializable structure """ return self._dynamic_template_data @dynamic_template_data.setter def dynamic_template_data(self, value): + if not isinstance(value, dict): + value = value.get() + self._dynamic_template_data = value def get(self): diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index b29d8b8c2..535a841f0 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -10,12 +10,11 @@ EmailMessage = message.Message from sendgrid.helpers.mail import ( - Asm, ApiKeyIncludedException, Attachment, BccSettings, - BypassListManagement, Category, ClickTracking, Content, CustomArg, - DynamicTemplateData, Email, FooterSettings, From, Ganalytics, Header, - Mail, MailSettings, OpenTracking, Personalization, SandBoxMode, Section, - SendGridException, SpamCheck, Subject, SubscriptionTracking, Substitution, - TrackingSettings, To, ValidateApiKey + Asm, Attachment, + ClickTracking, Content, + DynamicTemplateData, Email, From, + Mail, Personalization, + Subject, Substitution, To ) @@ -210,18 +209,18 @@ def test_multiple_emails_to_multiple_recipients(self): to_emails = [ To(email='test+to0@example.com', - name='Example Name 0', - substitutions=[ - Substitution('-name-', 'Example Name Substitution 0'), - Substitution('-github-', 'https://example.com/test0'), - ], - subject=Subject('Override Global Subject')), + name='Example Name 0', + substitutions=[ + Substitution('-name-', 'Example Name Substitution 0'), + Substitution('-github-', 'https://example.com/test0'), + ], + subject=Subject('Override Global Subject')), To(email='test+to1@example.com', - name='Example Name 1', - substitutions=[ - Substitution('-name-', 'Example Name Substitution 1'), - Substitution('-github-', 'https://example.com/test1'), - ]) + name='Example Name 1', + substitutions=[ + Substitution('-name-', 'Example Name Substitution 1'), + Substitution('-github-', 'https://example.com/test1'), + ]) ] global_substitutions = Substitution('-time-', '2019-01-01 00:00:00') message = Mail( @@ -285,6 +284,55 @@ def test_multiple_emails_to_multiple_recipients(self): }''') ) + def test_dynamic_template_data(self): + self.maxDiff = None + + to_emails = [ + To(email='test+to@example.com', + name='Example To Name', + dynamic_template_data=DynamicTemplateData({'name': 'Example Name'})) + ] + message = Mail( + from_email=From('test@example.com', 'Example From Name'), + to_emails=to_emails, + subject=Subject('Hi!'), + plain_text_content='Hello!', + html_content='Hello!') + + self.assertEqual( + message.get(), + json.loads(r'''{ + "content": [ + { + "type": "text/plain", + "value": "Hello!" + }, + { + "type": "text/html", + "value": "Hello!" + } + ], + "from": { + "email": "test@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "dynamic_template_data": { + "name": "Example Name" + }, + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Hi!" + }''') + ) + def test_kitchen_sink(self): from sendgrid.helpers.mail import ( Mail, From, To, Cc, Bcc, Subject, Substitution, Header, diff --git a/use_cases/send_multiple_emails_to_multiple_recipients.md b/use_cases/send_multiple_emails_to_multiple_recipients.md index 7217d0a0b..e3085469d 100644 --- a/use_cases/send_multiple_emails_to_multiple_recipients.md +++ b/use_cases/send_multiple_emails_to_multiple_recipients.md @@ -1,33 +1,30 @@ ```python import os -import json from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail, To to_emails = [ To(email='test+to0@example.com', name='Example Name 0', - substitutions={ - '-name-': 'Example Name Substitution 0', - '-github-': 'https://example.com/test0', + dynamic_template_data={ + 'name': 'Dynamic Name 0', + 'url': 'https://example.com/test0', }, subject='Override Global Subject'), To(email='test+to1@example.com', name='Example Name 1', - substitutions={ - '-name-': 'Example Name Substitution 1', - '-github-': 'https://example.com/test1', + dynamic_template_data={ + 'name': 'Dynamic Name 1', + 'url': 'https://example.com/test1', }), ] -global_substitutions = {'-time-': '2019-01-01 00:00:00'} message = Mail( from_email=('test+from@example.com', 'Example From Name'), to_emails=to_emails, - subject='Hi -name-, this is the global subject', - html_content='Hello -name-, your URL is ' + - 'here email sent at -time-', - global_substitutions=global_substitutions, + subject='Global subject', is_multiple=True) +message.template_id = 'd-12345678901234567890123456789012' + try: sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) response = sendgrid_client.send(message) @@ -36,4 +33,4 @@ try: print(response.headers) except Exception as e: print(e.message) -``` \ No newline at end of file +```