From e07db8d02269883dd42eda7af560ee2128d03425 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Tue, 26 Jun 2018 11:36:44 -0700 Subject: [PATCH 01/10] Initial working version of Send a Single Email to a Single Recipient --- live_test.py | 19 ++++++++++ proposals/mail-helper-refactor.md | 3 +- sendgrid/helpers/mail/__init__.py | 5 +++ sendgrid/helpers/mail/from_email.py | 4 ++ sendgrid/helpers/mail/html_content.py | 41 +++++++++++++++++++++ sendgrid/helpers/mail/mail.py | 20 ++++++---- sendgrid/helpers/mail/plain_text_content.py | 41 +++++++++++++++++++++ sendgrid/helpers/mail/subject.py | 38 +++++++++++++++++++ sendgrid/helpers/mail/to_email.py | 4 ++ sendgrid/sendgrid.py | 4 ++ 10 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 live_test.py create mode 100644 sendgrid/helpers/mail/from_email.py create mode 100644 sendgrid/helpers/mail/html_content.py create mode 100644 sendgrid/helpers/mail/plain_text_content.py create mode 100644 sendgrid/helpers/mail/subject.py create mode 100644 sendgrid/helpers/mail/to_email.py diff --git a/live_test.py b/live_test.py new file mode 100644 index 000000000..c432133b6 --- /dev/null +++ b/live_test.py @@ -0,0 +1,19 @@ +# Send a Single Email to a Single Recipient +import os +from sendgrid import SendGridAPIClient # import sendgrid +from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException # from sendgrid.helpers.mail + +msg = Mail(from_email=From('dx@sendgrid.com', 'DX'), + to_emails=To('elmer.thomas@sendgrid.com', 'Elmer Thomas'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), + html_content=HtmlContent('and easy to do anywhere, even with Python')) + +try: + sg_client = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + response = sg_client.send(request_body=msg) + print(response.status_code) + print(response.body) + print(response.headers) +except SendGridException as e: + print(e.message) \ No newline at end of file diff --git a/proposals/mail-helper-refactor.md b/proposals/mail-helper-refactor.md index 5a0127fd6..9162e4a4d 100644 --- a/proposals/mail-helper-refactor.md +++ b/proposals/mail-helper-refactor.md @@ -16,7 +16,8 @@ msg = Mail(from_email=From('from@example.com', 'From Name'), html_content=HtmlContent('and easy to do anywhere, even with Python')) try: - response = sendgrid.send(msg, apikey=os.environ.get('SENDGRID_apikey')) + sg_client = SendGridAPIClient() + response = sg_client.send(msg, apikey=os.environ.get('SENDGRID_apikey')) print(response.status_code) print(response.body) print(response.headers) diff --git a/sendgrid/helpers/mail/__init__.py b/sendgrid/helpers/mail/__init__.py index 1dd769e99..7a26aa411 100644 --- a/sendgrid/helpers/mail/__init__.py +++ b/sendgrid/helpers/mail/__init__.py @@ -9,16 +9,21 @@ from .email import Email from .exceptions import SendGridException, APIKeyIncludedException from .footer_settings import FooterSettings +from .from_email import From from .ganalytics import Ganalytics from .header import Header +from .html_content import HtmlContent from .mail_settings import MailSettings from .mail import Mail from .open_tracking import OpenTracking from .personalization import Personalization +from .plain_text_content import PlainTextContent from .sandbox_mode import SandBoxMode from .section import Section from .spam_check import SpamCheck +from .subject import Subject from .subscription_tracking import SubscriptionTracking from .substitution import Substitution from .tracking_settings import TrackingSettings +from .to_email import To from .validators import ValidateAPIKey diff --git a/sendgrid/helpers/mail/from_email.py b/sendgrid/helpers/mail/from_email.py new file mode 100644 index 000000000..c12eeb4ac --- /dev/null +++ b/sendgrid/helpers/mail/from_email.py @@ -0,0 +1,4 @@ +from .email import Email + +class From(Email): + """A from email address with an optional name.""" \ No newline at end of file diff --git a/sendgrid/helpers/mail/html_content.py b/sendgrid/helpers/mail/html_content.py new file mode 100644 index 000000000..271f7321b --- /dev/null +++ b/sendgrid/helpers/mail/html_content.py @@ -0,0 +1,41 @@ +from .content import Content +from .validators import ValidateAPIKey + +class HtmlContent(Content): + """HTML content to be included in your email. + """ + + def __init__(self, value): + """Create a HtmlContent with the specified MIME type and value. + + :param value: The actual HTML content. + :type value: string, optional + """ + self._type = "text/html" + self._value = value + self._validator = ValidateAPIKey() + + @property + def value(self): + """The actual HTML content. + + :rtype: string + """ + return self._value + + @value.setter + def value(self, value): + self._validator.validate_message_dict(value) + self._value = value + + def get(self): + """ + Get a JSON-ready representation of this HtmlContent. + + :returns: This HtmlContent, ready for use in a request body. + :rtype: dict + """ + content = {} + content["type"] = "text/html" + content["value"] = self._value + return content diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 6554d4508..1563f5c5c 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -2,7 +2,6 @@ from .personalization import Personalization from .header import Header - class Mail(object): """A request to be sent with the SendGrid v3 Mail Send API (v3/mail/send). @@ -10,9 +9,10 @@ class Mail(object): """ def __init__(self, from_email=None, + to_emails=None, subject=None, - to_email=None, - content=None): + plain_text_content=None, + html_content=None): """Create a Mail object. If any parameters are not supplied, they must be set after initialization. @@ -47,15 +47,18 @@ def __init__(self, self.from_email = from_email if subject: - self.subject = subject + self.subject = subject.get() - if to_email: + if to_emails: personalization = Personalization() - personalization.add_to(to_email) + personalization.add_to(to_emails) self.add_personalization(personalization) - if content: - self.add_content(content) + if plain_text_content: + self.add_content(plain_text_content) + + if html_content: + self.add_content(html_content) def __str__(self): """Get a JSON representation of this Mail request. @@ -395,3 +398,4 @@ def add_custom_arg(self, custom_arg): if self._custom_args is None: self._custom_args = [] self._custom_args.append(custom_arg) + diff --git a/sendgrid/helpers/mail/plain_text_content.py b/sendgrid/helpers/mail/plain_text_content.py new file mode 100644 index 000000000..a3048158b --- /dev/null +++ b/sendgrid/helpers/mail/plain_text_content.py @@ -0,0 +1,41 @@ +from .content import Content +from .validators import ValidateAPIKey + +class PlainTextContent(Content): + """Plain text content to be included in your email. + """ + + def __init__(self, value): + """Create a PlainTextContent with the specified MIME type and value. + + :param value: The actual text content. + :type value: string, optional + """ + self._type = "text/plain" + self._value = value + self._validator = ValidateAPIKey() + + @property + def value(self): + """The actual text content. + + :rtype: string + """ + return self._value + + @value.setter + def value(self, value): + self._validator.validate_message_dict(value) + self._value = value + + def get(self): + """ + Get a JSON-ready representation of this PlainTextContent. + + :returns: This PlainTextContent, ready for use in a request body. + :rtype: dict + """ + content = {} + content["type"] = "text/plain" + content["value"] = self._value + return content diff --git a/sendgrid/helpers/mail/subject.py b/sendgrid/helpers/mail/subject.py new file mode 100644 index 000000000..8fc3ad8b0 --- /dev/null +++ b/sendgrid/helpers/mail/subject.py @@ -0,0 +1,38 @@ +class Subject(object): + """A subject for an email message.""" + + def __init__(self, subject): + """Create a Subjuct. + + :param subject: The subject for an email + :type subject: string + """ + self._subject = subject + + @property + def subject(self): + """The subject of an email. + + :rtype: string + """ + return self._subject + + @subject.setter + def subject(self, value): + self._subject = value + + def __str__(self): + """Get a JSON representation of this Mail request. + + :rtype: string + """ + return str(self.get()) + + def get(self): + """ + Get a JSON-ready representation of this Subject. + + :returns: This Subject, ready for use in a request body. + :rtype: string + """ + return self._subject diff --git a/sendgrid/helpers/mail/to_email.py b/sendgrid/helpers/mail/to_email.py new file mode 100644 index 000000000..18ef8f725 --- /dev/null +++ b/sendgrid/helpers/mail/to_email.py @@ -0,0 +1,4 @@ +from .email import Email + +class To(Email): + """A to email address with an optional name.""" \ No newline at end of file diff --git a/sendgrid/sendgrid.py b/sendgrid/sendgrid.py index 0f09bd542..994c11f16 100644 --- a/sendgrid/sendgrid.py +++ b/sendgrid/sendgrid.py @@ -103,3 +103,7 @@ def api_key(self): @api_key.setter def api_key(self, value): self.apikey = value + + def send(self, request_body): + response = self.client.mail.send.post(request_body=request_body.get()) + return response From 6fb64da5e08ed8a7e510f918c98e25de87944ebf Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Thu, 12 Jul 2018 10:52:54 -0700 Subject: [PATCH 02/10] Code cleanup for first use case --- live_test.py | 25 ++++++++++++--------- proposals/mail-helper-refactor.md | 20 ++++++++--------- sendgrid/helpers/mail/html_content.py | 11 +++++---- sendgrid/helpers/mail/plain_text_content.py | 4 ++-- sendgrid/helpers/mail/subject.py | 2 +- sendgrid/sendgrid.py | 4 ++-- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/live_test.py b/live_test.py index c432133b6..97fa15816 100644 --- a/live_test.py +++ b/live_test.py @@ -1,19 +1,24 @@ # Send a Single Email to a Single Recipient import os -from sendgrid import SendGridAPIClient # import sendgrid -from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException # from sendgrid.helpers.mail +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException -msg = Mail(from_email=From('dx@sendgrid.com', 'DX'), - to_emails=To('elmer.thomas@sendgrid.com', 'Elmer Thomas'), - subject=Subject('Sending with SendGrid is Fun'), - plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), - html_content=HtmlContent('and easy to do anywhere, even with Python')) +message = Mail(from_email=From('dx@sendgrid.com', 'DX'), + to_emails=To('elmer.thomas@sendgrid.com', 'Elmer Thomas'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), + html_content=HtmlContent('and easy to do anywhere, even with Python')) try: - sg_client = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) - response = sg_client.send(request_body=msg) + sendgrid_client = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message=message) print(response.status_code) print(response.body) print(response.headers) except SendGridException as e: - print(e.message) \ No newline at end of file + print(e.message) + +# ToDo + +## The Mail constructor should also support passing in tuples and strings +## The send function parameter should be better named, maybe email_message or simply message diff --git a/proposals/mail-helper-refactor.md b/proposals/mail-helper-refactor.md index 9162e4a4d..68c3f403a 100644 --- a/proposals/mail-helper-refactor.md +++ b/proposals/mail-helper-refactor.md @@ -6,22 +6,22 @@ This is the minimum code needed to send an email. ```python import os -import sendgrid -from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException -msg = Mail(from_email=From('from@example.com', 'From Name'), - to_emails=To('to@example.com', 'To Name'), - subject=Subject('Sending with SendGrid is Fun'), - plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), - html_content=HtmlContent('and easy to do anywhere, even with Python')) +message = Mail(from_email=From('from@example.com', 'From Name'), + to_emails=To('to@example.com', 'To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), + html_content=HtmlContent('and easy to do anywhere, even with Python')) try: - sg_client = SendGridAPIClient() - response = sg_client.send(msg, apikey=os.environ.get('SENDGRID_apikey')) + sendgrid_client = SendGridAPIClient(apikey=os.environ.get('SENDGRID_apikey')) + response = sendgrid_client.send(message=message) print(response.status_code) print(response.body) print(response.headers) -except Exception as e: +except SendGridException as e: print(e.read()) ``` diff --git a/sendgrid/helpers/mail/html_content.py b/sendgrid/helpers/mail/html_content.py index 271f7321b..0c81f9ce6 100644 --- a/sendgrid/helpers/mail/html_content.py +++ b/sendgrid/helpers/mail/html_content.py @@ -2,18 +2,17 @@ from .validators import ValidateAPIKey class HtmlContent(Content): - """HTML content to be included in your email. - """ + """HTML content to be included in your email.""" def __init__(self, value): - """Create a HtmlContent with the specified MIME type and value. + """Create an HtmlContent with the specified MIME type and value. - :param value: The actual HTML content. + :param value: The HTML content. :type value: string, optional """ - self._type = "text/html" - self._value = value self._validator = ValidateAPIKey() + self.type = "text/html" + self.value = value @property def value(self): diff --git a/sendgrid/helpers/mail/plain_text_content.py b/sendgrid/helpers/mail/plain_text_content.py index a3048158b..9c55d12d9 100644 --- a/sendgrid/helpers/mail/plain_text_content.py +++ b/sendgrid/helpers/mail/plain_text_content.py @@ -11,9 +11,9 @@ def __init__(self, value): :param value: The actual text content. :type value: string, optional """ - self._type = "text/plain" - self._value = value self._validator = ValidateAPIKey() + self.type = "text/plain" + self.value = value @property def value(self): diff --git a/sendgrid/helpers/mail/subject.py b/sendgrid/helpers/mail/subject.py index 8fc3ad8b0..2a9841c16 100644 --- a/sendgrid/helpers/mail/subject.py +++ b/sendgrid/helpers/mail/subject.py @@ -7,7 +7,7 @@ def __init__(self, subject): :param subject: The subject for an email :type subject: string """ - self._subject = subject + self.subject = subject @property def subject(self): diff --git a/sendgrid/sendgrid.py b/sendgrid/sendgrid.py index 994c11f16..c044196a3 100644 --- a/sendgrid/sendgrid.py +++ b/sendgrid/sendgrid.py @@ -104,6 +104,6 @@ def api_key(self): def api_key(self, value): self.apikey = value - def send(self, request_body): - response = self.client.mail.send.post(request_body=request_body.get()) + def send(self, message): + response = self.client.mail.send.post(request_body=message.get()) return response From 4ee2dd679a43f925f08981af76714f57a612226d Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Thu, 12 Jul 2018 11:41:59 -0700 Subject: [PATCH 03/10] Integrate PR #486 --- sendgrid/helpers/mail/mail.py | 142 ++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 1563f5c5c..408f25819 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -74,68 +74,42 @@ def get(self): """ mail = {} - if self.from_email is not None: - mail["from"] = self.from_email.get() - - if self.subject is not None: - mail["subject"] = self.subject - - if self.personalizations: - mail["personalizations"] = [ - personalization.get() - for personalization in self.personalizations - ] + REQUEST_BODY_KEYS = [ + 'attachments', + 'batch_id', + 'categories', + 'custom_args', + 'headers', + 'ip_pool_name', + 'personalizations', + 'sections', + 'send_at', + 'subject', + 'template_id' + ] + + for key in REQUEST_BODY_KEYS: + value = getattr(self, key, None) + if value: + mail[key] = value if self.contents: - mail["content"] = [ob.get() for ob in self.contents] - - if self.attachments: - mail["attachments"] = [ob.get() for ob in self.attachments] - - if self.template_id is not None: - mail["template_id"] = self.template_id - - if self.sections: - sections = {} - for key in self.sections: - sections.update(key.get()) - mail["sections"] = sections - - if self.headers: - headers = {} - for key in self.headers: - headers.update(key.get()) - mail["headers"] = headers - - if self.categories: - mail["categories"] = [category.get() for category in - self.categories] - - if self.custom_args: - custom_args = {} - for key in self.custom_args: - custom_args.update(key.get()) - mail["custom_args"] = custom_args + mail["content"] = self.contents - if self.send_at is not None: - mail["send_at"] = self.send_at + if self.from_email: + mail["from"] = self.from_email.get() - if self.batch_id is not None: - mail["batch_id"] = self.batch_id - if self.asm is not None: + if self.asm: mail["asm"] = self.asm.get() - if self.ip_pool_name is not None: - mail["ip_pool_name"] = self.ip_pool_name - - if self.mail_settings is not None: + if self.mail_settings: mail["mail_settings"] = self.mail_settings.get() - if self.tracking_settings is not None: + if self.tracking_settings: mail["tracking_settings"] = self.tracking_settings.get() - if self.reply_to is not None: + if self.reply_to: mail["reply_to"] = self.reply_to.get() return mail @@ -282,9 +256,15 @@ def personalizations(self): message should be handled. A maximum of 1000 personalizations can be included. - :rtype: list + :returns: List of dictionaries. Each dictionary is obtained by + Personalization.get + :rtype: list(dictionaries) """ - return self._personalizations + if self._personalizations is not None: + return [ + personalization.get() + for personalization in self._personalizations + ] def add_personalization(self, personalizations): """Add a new Personalization to this Mail. @@ -296,10 +276,16 @@ def add_personalization(self, personalizations): @property def contents(self): """The Contents of this Mail. Must include at least one MIME type. - + + :returns: List of dictionaries returned by content.get :rtype: list(Content) """ - return self._contents + if self._contents is not None: + return[ + ob.get() + for ob in self._contents + ] + return None def add_content(self, content): """Add a new Content to this Mail. Usually the plaintext or HTML @@ -320,10 +306,16 @@ def add_content(self, content): def attachments(self): """The attachments included with this Mail. - :returns: List of Attachment objects. - :rtype: list(Attachment) + :returns: List of dictionaries. Each dictionary is obtained by + Attachment.get + :rtype: list(dictionaries) """ - return self._attachments + if self._attachments: + return [ + ob.get() + for ob in self._attachments + ] + return None def add_attachment(self, attachment): """Add an Attachment to this Mail. @@ -336,10 +328,16 @@ def add_attachment(self, attachment): def sections(self): """The sections included with this Mail. - :returns: List of Section objects. - :rtype: list(Section) + :returns: List of of dictionaries. Each dictionary is obtained by + Section.get + :rtype: list(dictionaries) """ - return self._sections + if self._sections: + sections = {} + for key in self._sections: + sections.update(key.get()) + return sections + return None def add_section(self, section): """Add a Section to this Mail. @@ -352,10 +350,16 @@ def add_section(self, section): def headers(self): """The Headers included with this Mail. - :returns: List of Header objects. - :rtype: list(Header) + :returns: List of dictionaries. Each dictionary is obtained by + Header.get + :rtype: list(dictionaries) """ - return self._headers + if self._headers: + headers = {} + for key in self._headers: + headers.update(key.get()) + return headers + return None def add_header(self, header): """Add a Header to this Mail. @@ -374,9 +378,15 @@ def add_header(self, header): def categories(self): """The Categories applied to this Mail. Must not exceed 10 items - :rtype: list(Category) + :returns: List of dictionaries. Each dictionary is obtained by + Category.get + :rtype: list(dictionaries) """ - return self._categories + if self._categories: + return [ + category.get + for category in self._categories + ] def add_category(self, category): """Add a Category to this Mail. Must be less than 255 characters. From 3b705ed408c0c28073d53d9638a8b3010cb2dfe0 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Thu, 12 Jul 2018 11:49:18 -0700 Subject: [PATCH 04/10] Integrate PR #458 --- sendgrid/helpers/mail/mail.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 408f25819..d70a0eca8 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -333,10 +333,7 @@ def sections(self): :rtype: list(dictionaries) """ if self._sections: - sections = {} - for key in self._sections: - sections.update(key.get()) - return sections + return self.update_objects(self._sections) return None def add_section(self, section): @@ -355,10 +352,7 @@ def headers(self): :rtype: list(dictionaries) """ if self._headers: - headers = {} - for key in self._headers: - headers.update(key.get()) - return headers + return self.update_objects(self._headers) return None def add_header(self, header): @@ -402,10 +396,20 @@ def custom_args(self): Must not exceed 10,000 characters. :rtype: list(CustomArg) """ - return self._custom_args + if self._custom_args: + return self.update_objects(self._custom_args) + return None def add_custom_arg(self, custom_arg): if self._custom_args is None: self._custom_args = [] self._custom_args.append(custom_arg) + + def update_objects(self, property): + objects = {} + for key in property: + objects.update(key.get()) + return objects + + From 7ed4338d67507a183b0897532aa3e22b58644374 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Thu, 12 Jul 2018 11:56:59 -0700 Subject: [PATCH 05/10] Integrate PR #493 --- sendgrid/helpers/mail/personalization.py | 53 +++++++++--------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 8bb4bed0b..49a779cf9 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -4,7 +4,7 @@ class Personalization(object): """ def __init__(self): - """Create an empty Personalization.""" + """Create an empty Personalization and initialize member variables.""" self._tos = [] self._ccs = [] self._bccs = [] @@ -166,36 +166,23 @@ def get(self): :rtype: dict """ personalization = {} - if self.tos: - personalization["to"] = self.tos - - if self.ccs: - personalization["cc"] = self.ccs - - if self.bccs: - personalization["bcc"] = self.bccs - - if self.subject is not None: - personalization["subject"] = self.subject - - if self.headers: - headers = {} - for key in self.headers: - headers.update(key) - personalization["headers"] = headers - - if self.substitutions: - substitutions = {} - for key in self.substitutions: - substitutions.update(key) - personalization["substitutions"] = substitutions - - if self.custom_args: - custom_args = {} - for key in self.custom_args: - custom_args.update(key) - personalization["custom_args"] = custom_args - - if self.send_at is not None: - personalization["send_at"] = self.send_at + + for key in ['tos', 'ccs', 'bccs']: + value = getattr(self, key) + if value: + personalization[key[:-1]] = value + + for key in ['subject', 'send_at']: + value = getattr(self, key) + if value: + personalization[key] = value + + for prop_name in ['headers', 'substitutions', 'custom_args']: + prop = getattr(self, prop_name) + if prop: + obj = {} + for key in prop: + obj.update(key) + personalization[prop_name] = obj + return personalization From 6c232045fc7cee179aeae16d75bdaaaad6cd09f4 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 25 Jul 2018 10:05:47 -0700 Subject: [PATCH 06/10] Add PR #496 --- sendgrid/helpers/mail/mail.py | 445 +++++++--------------------------- 1 file changed, 86 insertions(+), 359 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index d70a0eca8..8813337e9 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -3,413 +3,140 @@ from .header import Header class Mail(object): - """A request to be sent with the SendGrid v3 Mail Send API (v3/mail/send). - - Use get() to get the request body. - """ - def __init__(self, - from_email=None, - to_emails=None, - subject=None, - plain_text_content=None, - html_content=None): - """Create a Mail object. - - If any parameters are not supplied, they must be set after initialization. - :param from_email: Email address to send from. - :type from_email: Email, optional - :param subject: Subject line of emails. - :type subject: string, optional - :param to_email: Email address to send to. - :type to_email: Email, optional - :param content: Content of the message. - :type content: Content, optional - """ - self._from_email = None - self._subject = None - self._template_id = None - self._send_at = None - self._batch_id = None - self._asm = None - self._ip_pool_name = None - self._mail_settings = None - self._tracking_settings = None - self._reply_to = None - self._personalizations = [] - self._contents = [] - self._attachments = [] - self._sections = [] - self._headers = [] - self._categories = [] - self._custom_args = [] - - if from_email: - self.from_email = from_email - - if subject: - self.subject = subject.get() - - if to_emails: + """Creates the response body for v3/mail/send""" + def __init__( + self, from_email=None, subject=None, to_email=None, content=None): + self._personalizations = None + self._contents = None + self._attachments = None + self._sections = None + self._headers = None + self._categories = None + self._custom_args = None + self.from_email = None + self.subject = None + self.template_id = None + self.send_at = None + self.batch_id = None + self.asm = None + self.ip_pool_name = None + self.mail_settings = None + self.tracking_settings = None + self.reply_to = None + + # Minimum required to send a single email + if to_email: personalization = Personalization() - personalization.add_to(to_emails) + personalization.add_to(to_email) self.add_personalization(personalization) - - if plain_text_content: - self.add_content(plain_text_content) - - if html_content: - self.add_content(html_content) + if subject: + self.subject = subject + if from_email: + self.from_email = from_email + if content: + self.add_content(content) def __str__(self): - """Get a JSON representation of this Mail request. - - :rtype: string - """ return str(self.get()) def get(self): - """Get a response body for this Mail. - - :rtype: dict """ - mail = {} - - REQUEST_BODY_KEYS = [ - 'attachments', - 'batch_id', - 'categories', - 'custom_args', - 'headers', - 'ip_pool_name', - 'personalizations', - 'sections', - 'send_at', - 'subject', - 'template_id' - ] - - for key in REQUEST_BODY_KEYS: - value = getattr(self, key, None) - if value: - mail[key] = value - - if self.contents: - mail["content"] = self.contents - - if self.from_email: - mail["from"] = self.from_email.get() - - - if self.asm: - mail["asm"] = self.asm.get() - - if self.mail_settings: - mail["mail_settings"] = self.mail_settings.get() - - if self.tracking_settings: - mail["tracking_settings"] = self.tracking_settings.get() - - if self.reply_to: - mail["reply_to"] = self.reply_to.get() - - return mail - - @property - def from_email(self): - """The email from which this Mail will be sent. - - :rtype: string - """ - return self._from_email - - @from_email.setter - def from_email(self, value): - self._from_email = value - - @property - def subject(self): - """The global, or "message level", subject of this Mail. - - This may be overridden by personalizations[x].subject. - :rtype: string - """ - return self._subject - - @subject.setter - def subject(self, value): - self._subject = value - - @property - def template_id(self): - """The id of a template that you would like to use. - - If you use a template that contains a subject and content (either text - or html), you do not need to specify those at the personalizations nor - message level. - - :rtype: int - """ - - return self._template_id - - @template_id.setter - def template_id(self, value): - self._template_id = value - - @property - def send_at(self): - """A unix timestamp allowing you to specify when you want your email to - be delivered. This may be overridden by the personalizations[x].send_at - parameter. Scheduling more than 72 hours in advance is forbidden. - - :rtype: int - """ - return self._send_at - - @send_at.setter - def send_at(self, value): - self._send_at = value - - @property - def batch_id(self): - """An ID for this batch of emails. - - This represents a batch of emails sent at the same time. Including a - batch_id in your request allows you include this email in that batch, - and also enables you to cancel or pause the delivery of that batch. - For more information, see https://sendgrid.com/docs/API_Reference/Web_API_v3/cancel_schedule_send.html - - :rtype: int - """ - return self._batch_id - - @batch_id.setter - def batch_id(self, value): - self._batch_id = value - - @property - def asm(self): - """The ASM for this Mail. - - :rtype: ASM - """ - return self._asm - - @asm.setter - def asm(self, value): - self._asm = value - - @property - def mail_settings(self): - """The MailSettings for this Mail. - - :rtype: MailSettings - """ - return self._mail_settings - - @mail_settings.setter - def mail_settings(self, value): - self._mail_settings = value - - @property - def tracking_settings(self): - """The TrackingSettings for this Mail. - - :rtype: TrackingSettings - """ - return self._tracking_settings - - @tracking_settings.setter - def tracking_settings(self, value): - self._tracking_settings = value - - @property - def ip_pool_name(self): - """The IP Pool that you would like to send this Mail email from. - - :rtype: string - """ - return self._ip_pool_name - - @ip_pool_name.setter - def ip_pool_name(self, value): - self._ip_pool_name = value - - @property - def reply_to(self): - """The email address to use in the Reply-To header. - - :rtype: Email - """ - return self._reply_to - - @reply_to.setter - def reply_to(self, value): - self._reply_to = value + :return: request body dict + """ + mail = { + 'from': self._get_or_none(self.from_email), + 'subject': self.subject, + 'personalizations': [p.get() for p in self.personalizations or []], + 'content': [c.get() for c in self.contents or []], + 'attachments': [a.get() for a in self.attachments or []], + 'template_id': self.template_id, + 'sections': self._flatten_dictionaries(self.sections), + 'headers': self._flatten_dictionaries(self.headers), + 'categories': [c.get() for c in self.categories or []], + 'custom_args': self._flatten_dictionaries(self.custom_args), + 'send_at': self.send_at, + 'batch_id': self.batch_id, + 'asm': self._get_or_none(self.asm), + 'ip_pool_name': self.ip_pool_name, + 'mail_settings': self._get_or_none(self.mail_settings), + 'tracking_settings': self._get_or_none(self.tracking_settings), + 'reply_to': self._get_or_none(self.reply_to), + } + + return dict((key, value) for key, value in mail.items() + if value is not None and value != [] and value != {}) + + def _get_or_none(self, from_obj): + return from_obj.get() if from_obj is not None else None + + def _flatten_dictionaries(self, dicts): + list_of_dicts = [d.get() for d in dicts or []] + return dict((k, v) for d in list_of_dicts for k, v in d.items()) @property def personalizations(self): - """The Personalizations applied to this Mail. - - Each object within personalizations can be thought of as an envelope - - it defines who should receive an individual message and how that - message should be handled. A maximum of 1000 personalizations can be - included. - - :returns: List of dictionaries. Each dictionary is obtained by - Personalization.get - :rtype: list(dictionaries) - """ - if self._personalizations is not None: - return [ - personalization.get() - for personalization in self._personalizations - ] + return self._personalizations def add_personalization(self, personalizations): - """Add a new Personalization to this Mail. - - :type personalizations: Personalization - """ - self._personalizations.append(personalizations) + self._personalizations = self._ensure_append( + personalizations, self._personalizations) @property def contents(self): - """The Contents of this Mail. Must include at least one MIME type. - - :returns: List of dictionaries returned by content.get - :rtype: list(Content) - """ - if self._contents is not None: - return[ - ob.get() - for ob in self._contents - ] - return None + return self._contents def add_content(self, content): - """Add a new Content to this Mail. Usually the plaintext or HTML - message contents. - - :type content: Content - """ - if self._contents is None: - self._contents = [] - # Text content should be before HTML content if content._type == "text/plain": - self._contents.insert(0, content) + self._contents = self._ensure_insert(content, self._contents) else: - self._contents.append(content) + self._contents = self._ensure_append(content, self._contents) @property def attachments(self): - """The attachments included with this Mail. - - :returns: List of dictionaries. Each dictionary is obtained by - Attachment.get - :rtype: list(dictionaries) - """ - if self._attachments: - return [ - ob.get() - for ob in self._attachments - ] - return None + return self._attachments def add_attachment(self, attachment): - """Add an Attachment to this Mail. - - :type attachment: Attachment - """ - self._attachments.append(attachment) + self._attachments = self._ensure_append(attachment, self._attachments) @property def sections(self): - """The sections included with this Mail. - - :returns: List of of dictionaries. Each dictionary is obtained by - Section.get - :rtype: list(dictionaries) - """ - if self._sections: - return self.update_objects(self._sections) - return None + return self._sections def add_section(self, section): - """Add a Section to this Mail. - - :type section: Section - """ - self._sections.append(section) + self._sections = self._ensure_append(section, self._sections) @property def headers(self): - """The Headers included with this Mail. - - :returns: List of dictionaries. Each dictionary is obtained by - Header.get - :rtype: list(dictionaries) - """ - if self._headers: - return self.update_objects(self._headers) - return None + return self._headers def add_header(self, header): - """Add a Header to this Mail. - - The header provided can be a Header or a dictionary with a single - key-value pair. - :type header: object - """ if isinstance(header, dict): (k, v) = list(header.items())[0] - self._headers.append(Header(k, v)) + self._headers = self._ensure_append(Header(k, v), self._headers) else: - self._headers.append(header) + self._headers = self._ensure_append(header, self._headers) @property def categories(self): - """The Categories applied to this Mail. Must not exceed 10 items - - :returns: List of dictionaries. Each dictionary is obtained by - Category.get - :rtype: list(dictionaries) - """ - if self._categories: - return [ - category.get - for category in self._categories - ] + return self._categories def add_category(self, category): - """Add a Category to this Mail. Must be less than 255 characters. - - :type category: string - """ - self._categories.append(category) + self._categories = self._ensure_append(category, self._categories) @property def custom_args(self): - """The CustomArgs attached to this Mail. - - Must not exceed 10,000 characters. - :rtype: list(CustomArg) - """ - if self._custom_args: - return self.update_objects(self._custom_args) - return None + return self._custom_args def add_custom_arg(self, custom_arg): - if self._custom_args is None: - self._custom_args = [] - self._custom_args.append(custom_arg) - - def update_objects(self, property): - objects = {} - for key in property: - objects.update(key.get()) - return objects + self._custom_args = self._ensure_append(custom_arg, self._custom_args) - + def _ensure_append(self, new_items, append_to): + append_to = append_to or [] + append_to.append(new_items) + return append_to + def _ensure_insert(self, new_items, insert_to): + insert_to = insert_to or [] + insert_to.insert(0, new_items) + return insert_to \ No newline at end of file From 2ee538f00c535b5ce0c04cb6c68cc472b507e3b7 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 25 Jul 2018 11:11:38 -0700 Subject: [PATCH 07/10] Formatting --- sendgrid/helpers/mail/mail.py | 154 +++++++++++++++++----------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 8813337e9..b0ac616f7 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -6,80 +6,76 @@ class Mail(object): """Creates the response body for v3/mail/send""" def __init__( self, from_email=None, subject=None, to_email=None, content=None): - self._personalizations = None - self._contents = None self._attachments = None - self._sections = None - self._headers = None self._categories = None + self._contents = None self._custom_args = None - self.from_email = None - self.subject = None - self.template_id = None - self.send_at = None - self.batch_id = None + self._headers = None + self._personalizations = None + self._sections = None self.asm = None + self.batch_id = None + self.from_email = None self.ip_pool_name = None self.mail_settings = None - self.tracking_settings = None self.reply_to = None + self.send_at = None + self.subject = None + self.template_id = None + self.tracking_settings = None # Minimum required to send a single email + if from_email: + self.from_email = from_email + if subject: + self.subject = subject if to_email: personalization = Personalization() personalization.add_to(to_email) self.add_personalization(personalization) - if subject: - self.subject = subject - if from_email: - self.from_email = from_email if content: self.add_content(content) def __str__(self): return str(self.get()) - def get(self): - """ - :return: request body dict - """ - mail = { - 'from': self._get_or_none(self.from_email), - 'subject': self.subject, - 'personalizations': [p.get() for p in self.personalizations or []], - 'content': [c.get() for c in self.contents or []], - 'attachments': [a.get() for a in self.attachments or []], - 'template_id': self.template_id, - 'sections': self._flatten_dictionaries(self.sections), - 'headers': self._flatten_dictionaries(self.headers), - 'categories': [c.get() for c in self.categories or []], - 'custom_args': self._flatten_dictionaries(self.custom_args), - 'send_at': self.send_at, - 'batch_id': self.batch_id, - 'asm': self._get_or_none(self.asm), - 'ip_pool_name': self.ip_pool_name, - 'mail_settings': self._get_or_none(self.mail_settings), - 'tracking_settings': self._get_or_none(self.tracking_settings), - 'reply_to': self._get_or_none(self.reply_to), - } + def _ensure_append(self, new_items, append_to): + append_to = append_to or [] + append_to.append(new_items) + return append_to - return dict((key, value) for key, value in mail.items() - if value is not None and value != [] and value != {}) + def _ensure_insert(self, new_items, insert_to): + insert_to = insert_to or [] + insert_to.insert(0, new_items) + return insert_to + + def _flatten_dicts(self, dicts): + list_of_dicts = [d.get() for d in dicts or []] + return dict((k, v) for d in list_of_dicts for k, v in d.items()) def _get_or_none(self, from_obj): return from_obj.get() if from_obj is not None else None - def _flatten_dictionaries(self, dicts): - list_of_dicts = [d.get() for d in dicts or []] - return dict((k, v) for d in list_of_dicts for k, v in d.items()) + @property + def attachments(self): + return self._attachments + + def add_attachment(self, attachment): + self._attachments = self._ensure_append(attachment, self._attachments) @property - def personalizations(self): - return self._personalizations + def categories(self): + return self._categories - def add_personalization(self, personalizations): - self._personalizations = self._ensure_append( - personalizations, self._personalizations) + def add_category(self, category): + self._categories = self._ensure_append(category, self._categories) + + @property + def custom_args(self): + return self._custom_args + + def add_custom_arg(self, custom_arg): + self._custom_args = self._ensure_append(custom_arg, self._custom_args) @property def contents(self): @@ -92,20 +88,6 @@ def add_content(self, content): else: self._contents = self._ensure_append(content, self._contents) - @property - def attachments(self): - return self._attachments - - def add_attachment(self, attachment): - self._attachments = self._ensure_append(attachment, self._attachments) - - @property - def sections(self): - return self._sections - - def add_section(self, section): - self._sections = self._ensure_append(section, self._sections) - @property def headers(self): return self._headers @@ -118,25 +100,43 @@ def add_header(self, header): self._headers = self._ensure_append(header, self._headers) @property - def categories(self): - return self._categories + def personalizations(self): + return self._personalizations - def add_category(self, category): - self._categories = self._ensure_append(category, self._categories) + def add_personalization(self, personalizations): + self._personalizations = self._ensure_append( + personalizations, self._personalizations) @property - def custom_args(self): - return self._custom_args + def sections(self): + return self._sections - def add_custom_arg(self, custom_arg): - self._custom_args = self._ensure_append(custom_arg, self._custom_args) + def add_section(self, section): + self._sections = self._ensure_append(section, self._sections) - def _ensure_append(self, new_items, append_to): - append_to = append_to or [] - append_to.append(new_items) - return append_to + def get(self): + """ + :return: request body dict + """ + mail = { + 'from': self._get_or_none(self.from_email), + 'subject': self.subject, + 'personalizations': [p.get() for p in self.personalizations or []], + 'content': [c.get() for c in self.contents or []], + 'attachments': [a.get() for a in self.attachments or []], + 'template_id': self.template_id, + 'sections': self._flatten_dicts(self.sections), + 'headers': self._flatten_dicts(self.headers), + 'categories': [c.get() for c in self.categories or []], + 'custom_args': self._flatten_dicts(self.custom_args), + 'send_at': self.send_at, + 'batch_id': self.batch_id, + 'asm': self._get_or_none(self.asm), + 'ip_pool_name': self.ip_pool_name, + 'mail_settings': self._get_or_none(self.mail_settings), + 'tracking_settings': self._get_or_none(self.tracking_settings), + 'reply_to': self._get_or_none(self.reply_to), + } - def _ensure_insert(self, new_items, insert_to): - insert_to = insert_to or [] - insert_to.insert(0, new_items) - return insert_to \ No newline at end of file + return dict((key, value) for key, value in mail.items() + if value is not None and value != [] and value != {}) From 56a0ad09e7343e0c95321931bea0745927ad61ed Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 25 Jul 2018 11:16:47 -0700 Subject: [PATCH 08/10] Don't forget to thank previous PRs --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1869666..52f1479e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Change Log All notable changes to this project will be documented in this file. +## [6.0.0] - TBD + +- https://github.com/sendgrid/sendgrid-python/pull/486 +- https://github.com/sendgrid/sendgrid-python/pull/488 +- https://github.com/sendgrid/sendgrid-python/pull/493 +- https://github.com/sendgrid/sendgrid-python/pull/496 +- https://github.com/sendgrid/sendgrid-python/pull/509 +- https://github.com/sendgrid/sendgrid-python/pull/510 +- https://github.com/sendgrid/sendgrid-python/pull/512 +- https://github.com/sendgrid/sendgrid-python/pull/524 + ## [5.4.1] - 2018-06-26 ## ### Fixed - [PR #585](https://github.com/sendgrid/sendgrid-python/pull/585): Fix typo in `mail_example.py`. Big thanks to [Anurag Anand](https://github.com/theanuraganand) for the PR! From 918bbc7d1bf5a05eaa7dda287203d8e15fd334ba Mon Sep 17 00:00:00 2001 From: rahulkumaran Date: Wed, 3 Oct 2018 09:08:46 +0530 Subject: [PATCH 09/10] upgraded requirements.txt versions --- requirements.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index ceae6cf62..9d698f345 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Flask==0.10.1 -PyYAML==3.11 -python-http-client==2.2.1 -six==1.10.0 -pytest=3.7.1 \ No newline at end of file +Flask==1.0.2 +PyYAML==3.13 +python-http-client==3.1.0 +six==1.11.0 +pytest=3.8.2 From 21ec8fc97ebe1213063f0e01f0a21f7bf4ea401b Mon Sep 17 00:00:00 2001 From: Rahul Arulkumaran Date: Wed, 3 Oct 2018 09:16:18 +0530 Subject: [PATCH 10/10] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d698f345..a6646a692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ Flask==1.0.2 PyYAML==3.13 python-http-client==3.1.0 six==1.11.0 -pytest=3.8.2 +pytest==3.8.2