From b94fa7ad53fa821f80ae07a496e87c2d578e79bf Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 18 Aug 2020 10:11:13 -0500 Subject: [PATCH 001/149] docs: clean up and reconcile first timers documentation --- FIRST_TIMERS.md | 148 ++++++++++++++++++---------------- static/img/github-fork.png | Bin 0 -> 15189 bytes static/img/github-sign-up.png | Bin 0 -> 116981 bytes 3 files changed, 79 insertions(+), 69 deletions(-) create mode 100644 static/img/github-fork.png create mode 100644 static/img/github-sign-up.png diff --git a/FIRST_TIMERS.md b/FIRST_TIMERS.md index 4bf1b26e3..f6406f783 100644 --- a/FIRST_TIMERS.md +++ b/FIRST_TIMERS.md @@ -1,69 +1,79 @@ -## Welcome to the Twilio SendGrid Open Source Community -If you are new to Open Source, you are at the right place to start with. Contributions are always encouraged & appreciated. Just follow the organisation's Contribution Policies & you are good to go. - -## How to get Started? -- [Explore Twilio SendGrid](#explore) -- [Raise Issues(If Found Any)](#issues) -- [Setting up the Development Environment](#setup) -- [Proposing Change through a Pull Request](#pr) -- [Be Patient & Wait for reviews](#reviews) - - -### Explore Twilio SendGrid -Step 1: Get yourself Access to Twilio SendGrid API Service absolutely free from [here](https://sendgrid.com/free/?source=sendgrid-python) \ -Step 2: Get familiar with Twilio SendGrid Service -- Prerequisites are Python version 2.6, 2.7, 3.4, 3.5 or 3.6 -- Set up your [Twilio SendGrid API Key](https://app.sendgrid.com/settings/api_keys) to your local workspace [using](https://github.com/sendgrid/sendgrid-python#setup-environment-variables) -- Install Twilio SendGrid to your workspace using `pip install sendgrid` -- Copy & Run few sample programs from [here](https://github.com/sendgrid/sendgrid-python#hello-email) - - -### Raise Issues -Twilio SendGrid uses GitHub as the content management service so, all the issues related to the project be it some feature request or a bug report, all are reported at the [GitHub Issue Tracker](https://github.com/sendgrid/sendgrid-python/issues)\ -Kindly make sure, to check for any duplicate issues raised by fellow contributors before opening a new issue. Be humble & polite while commenting on issues -- Feature Request\ - In case you feel like something is missing or lacking in the API Service, feel free to share your views & opinions with the community -- Bug Report\ - If you encounter any sort of bug or abnormal behavior, feel free to inform the community after performing the following checks: - - Update to the latest version & check if the bug persists - - Check the Issue Tracker for any similar bug report - - Finally, fill up the Bug Report Template & Open the Issue highlighting your encountered bug & detailed steps to regenerate the bug. - - -### Setting up the Development Environment -- **Setting up Locally** - - Step 1: Install the Prerequistes: Any Version of Python (2.6 through 3.6) and both [python_http_client](https://github.com/sendgrid/python-http-client) and [ecdsa_python](https://github.com/starkbank/ecdsa-python) - - Step 2: Get a local copy of repository using `git clone https://github.com/sendgrid/sendgrid-python.git` - - Step 3: Set your [Twilio SendGrid API Key](https://app.sendgrid.com/settings/api_keys) to your local workspace using\ - `echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env`\ - `echo "sendgrid.env" >> .gitignore`\ - `source ./sendgrid.env` - - Step 4: The entire codebase consist of 3 major divisions -- **/examples** contains *Working examples that demonstrate usage* -- **/tests** contains *the unit and profiling tests* -- **/sendgrid** contains *the Web API v3 client ie sendgrid.py and other files*. - - -## Proposing Change through a Pull Request -**Step 1:** Fork the project & Clone your fork using `git clone https://github.com//sendgrid-python.git` - -**Step 2:** Reconfigure the remotes using `cd sendgrid-python` and `git remote add upstream https://github.com/sendgrid/sendgrid-python.git` - -**Step 3:** Create a new branch for your modifications using `git checkout -b ` - -**Step 4:** Commit the changes in logical chunks & add commit messages strictly following [this](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - -**Step 5:** Run all test locally, [for more info](CONTRIBUTING.md#testing) - -**Step 6:** Locally merge your the upstream development branch into your topic-branch using `git pull [--rebase] upstream main` - -**Step 7:** Push the topic branch up to your fork using `git push origin ` - -**Step 8:** Open a Pull Request with clear title and description against the main branch. - - -## Be Patient & Wait for Reviews -Kindly be patient & follow the suggestions as provided by the peer reviewers. Make required amendments & changes to the PR as asked. - -## [Explore New Issues to work upon](https://github.com/sendgrid/sendgrid-python/labels/difficulty%3A%20easy) +# How To Contribute to Twilio SendGrid Repositories via GitHub +Contributing to the Twilio SendGrid repositories is easy! All you need to do is find an open issue (see the bottom of this page for a list of repositories containing open issues), fix it and submit a pull request. Once you have submitted your pull request, the team can easily review it before it is merged into the repository. + +To make a pull request, follow these steps: + +1. Log into GitHub. If you do not already have a GitHub account, you will have to create one in order to submit a change. Click the Sign up link in the upper right-hand corner to create an account. Enter your username, password, and email address. If you are an employee of Twilio SendGrid, please use your full name with your GitHub account and enter Twilio SendGrid as your company so we can easily identify you. + + + +2. __[Fork](https://help.github.com/fork-a-repo/)__ the [sendgrid-python](https://github.com/sendgrid/sendgrid-python) repository: + + + +3. __Clone__ your fork via the following commands: + +```bash +# Clone your fork of the repo into the current directory +git clone https://github.com/your_username/sendgrid-python +# Navigate to the newly cloned directory +cd sendgrid-python +# Assign the original repo to a remote called "upstream" +git remote add upstream https://github.com/sendgrid/sendgrid-python +``` + +> Don't forget to replace *your_username* in the URL by your real GitHub username. + +4. __Create a new topic branch__ (off the main project development branch) to contain your feature, change, or fix: + +```bash +git checkout -b +``` + +5. __Commit your changes__ in logical chunks. + +Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up your commits before making them public. Probably you will also have to create tests (if needed) or create or update the example code that demonstrates the functionality of this change to the code. + +6. __Locally merge (or rebase)__ the upstream development branch into your topic branch: + +```bash +git pull [--rebase] upstream main +``` + +7. __Push__ your topic branch up to your fork: + +```bash +git push origin +``` + +8. __[Open a Pull Request](https://help.github.com/articles/creating-a-pull-request/#changing-the-branch-range-and-destination-repository/)__ with a clear title and description against the `main` branch. All tests must be passing before we will review the PR. + +## Important notice + +Before creating a pull request, make sure that you respect the repository's constraints regarding contributions. You can find them in the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## Repositories with Open, Easy, Help Wanted, Issue Filters + +* [Python SDK](https://github.com/sendgrid/sendgrid-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [PHP SDK](https://github.com/sendgrid/sendgrid-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [C# SDK](https://github.com/sendgrid/sendgrid-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Ruby SDK](https://github.com/sendgrid/sendgrid-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Node.js SDK](https://github.com/sendgrid/sendgrid-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Java SDK](https://github.com/sendgrid/sendgrid-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Go SDK](https://github.com/sendgrid/sendgrid-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Python STMPAPI Client](https://github.com/sendgrid/smtpapi-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [PHP STMPAPI Client](https://github.com/sendgrid/smtpapi-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [C# STMPAPI Client](https://github.com/sendgrid/smtpapi-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Ruby STMPAPI Client](https://github.com/sendgrid/smtpapi-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Node.js STMPAPI Client](https://github.com/sendgrid/smtpapi-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Java STMPAPI Client](https://github.com/sendgrid/smtpapi-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Go STMPAPI Client](https://github.com/sendgrid/smtpapi-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Python HTTP Client](https://github.com/sendgrid/python-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [PHP HTTP Client](https://github.com/sendgrid/php-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [C# HTTP Client](https://github.com/sendgrid/csharp-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Java HTTP Client](https://github.com/sendgrid/java-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Ruby HTTP Client](https://github.com/sendgrid/ruby-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Go HTTP Client](https://github.com/sendgrid/rest/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Open API Definition](https://github.com/sendgrid/sendgrid-oai/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [DX Automator](https://github.com/sendgrid/dx-automator/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Documentation](https://github.com/sendgrid/docs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) diff --git a/static/img/github-fork.png b/static/img/github-fork.png new file mode 100644 index 0000000000000000000000000000000000000000..6503be362193808dc19bce69d8ccdf518d9ef93f GIT binary patch literal 15189 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 Oll& Date: Wed, 19 Aug 2020 18:54:23 +0000 Subject: [PATCH 002/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c367217e7..d01488a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2020-08-19] Version 6.4.6 +-------------------------- +**Library - Chore** +- [PR #929](https://github.com/sendgrid/sendgrid-python/pull/929): update GitHub branch references to use HEAD. Thanks to [@thinkingserious](https://github.com/thinkingserious)! + + [2020-08-05] Version 6.4.5 -------------------------- **Library - Docs** From bf2aee6b23ab486b86bae4a8121e629b4ef60b33 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 19 Aug 2020 19:45:02 +0000 Subject: [PATCH 003/149] Release 6.4.6 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 9bf999671..a8b77c27e 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.4.5' +__version__ = '6.4.6' From f9d0513aacfa8293920d472aa744524417b5c5f2 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Mon, 24 Aug 2020 09:57:48 -0500 Subject: [PATCH 004/149] docs: remove roadmap/milestone sections for CONTRIBUTING and README --- CONTRIBUTING.md | 3 --- README.md | 6 ------ README.rst | 8 -------- 3 files changed, 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92d72897e..c2769176d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,9 +17,6 @@ All third party contributors acknowledge that any contributions they provide wil - [Creating a Pull Request](#creating-a-pull-request) - [Code Reviews](#code-reviews) - -We use [Milestones](https://github.com/sendgrid/sendgrid-python/milestones) to help define current roadmaps, please feel free to grab an issue from the current milestone. Please indicate that you have begun work on it to avoid collisions. Once a PR is made, community reviews, comments, suggestions, and additional PRs are welcomed and encouraged. - There are a few ways to contribute, which we'll enumerate below: ## Feature Request diff --git a/README.md b/README.md index d8049ca23..f6f8c76bc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ We appreciate your continued support, thank you! * [Usage](#usage) * [Use Cases](#use-cases) * [Announcements](#announcements) -* [Roadmap](#roadmap) * [How to Contribute](#contribute) * [Troubleshooting](#troubleshooting) * [About](#about) @@ -194,11 +193,6 @@ Please see our announcement regarding [breaking changes](https://github.com/send All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). You may also subscribe to email [release notifications](https://dx.sendgrid.com/newsletter/java) for releases and breaking changes. - -# Roadmap - -If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/sendgrid/sendgrid-python/issues) and [pull requests](https://github.com/sendgrid/sendgrid-python/pulls). We would love to hear your feedback. - # How to Contribute diff --git a/README.rst b/README.rst index 88d0fb280..cd2c6bbac 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,6 @@ Table of Contents - `General Usage <#usage>`__ - `Processing Inbound Email <#processing-inbound-email>`__ - `Announcements <#announcements>`__ -- `Roadmap <#roadmap>`__ - `How to Contribute <#how-to-contribute>`__ - `Troubleshooting <#troubleshooting>`__ - `About <#about>`__ @@ -226,13 +225,6 @@ Announcements All updates to this library are documented in our `CHANGELOG`_ and `releases`_. You may also subscribe to email `release notifications`_ for releases and breaking changes. -Roadmap -======= - -If you are interested in the future direction of this project, -please take a look at our open `issues`_ and `pull requests `__. -We would love to hear your feedback. - How to Contribute ================= From fd8375601c7f6a76738f18a7ae194ccb2f720ae3 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Fri, 28 Aug 2020 15:29:02 -0700 Subject: [PATCH 005/149] chore: move encrypted tokens to environment variables --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c158b2b3..51b69d9d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,5 +38,4 @@ notifications: on_pull_requests: false on_success: never on_failure: change - rooms: - - secure: Yp7gJ6NPRPNgO77vwS0HynSdnU5LYlLlUNBEzcx+zy230UxuLLWcYZtIqsIqt4oZm45OwgJLBwoCMgmU2Jcj79rGyqWKYtUcLMLKgHVzSgxjm2outt2fxjXIJHIU60S3RCGofBJRkPwEMb7ibgwHYBEsH3wIeLrVVbWvimxka6A= + rooms: $SLACK_TOKEN From 9392eabd2d0f9d0eae4321448c4f91dc578cc08b Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Tue, 1 Sep 2020 13:23:18 -0700 Subject: [PATCH 006/149] Revert "chore: move encrypted tokens to environment variables" This reverts commit fd837560 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 51b69d9d2..8c158b2b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,4 +38,5 @@ notifications: on_pull_requests: false on_success: never on_failure: change - rooms: $SLACK_TOKEN + rooms: + - secure: Yp7gJ6NPRPNgO77vwS0HynSdnU5LYlLlUNBEzcx+zy230UxuLLWcYZtIqsIqt4oZm45OwgJLBwoCMgmU2Jcj79rGyqWKYtUcLMLKgHVzSgxjm2outt2fxjXIJHIU60S3RCGofBJRkPwEMb7ibgwHYBEsH3wIeLrVVbWvimxka6A= From 6606e8de915e873ace7e01e195e9f464b20609f1 Mon Sep 17 00:00:00 2001 From: Arbitrage0 <24595000+Arbitrage0@users.noreply.github.com> Date: Thu, 10 Sep 2020 20:54:16 +0100 Subject: [PATCH 007/149] docs: correct attachment example (#936) --- examples/helpers/mail_example.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/helpers/mail_example.py b/examples/helpers/mail_example.py index de1648df9..700970110 100644 --- a/examples/helpers/mail_example.py +++ b/examples/helpers/mail_example.py @@ -83,10 +83,10 @@ def build_attachment1(): """Build attachment mock. Make sure your content is base64 encoded before passing into attachment.content. Another example: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/attachment.md""" attachment = Attachment() - attachment.content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" + attachment.file_content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" "Y3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQ3JhcyBwdW12") - attachment.type = "application/pdf" - attachment.filename = "balance_001.pdf" + attachment.file_type = "application/pdf" + attachment.file_name = "balance_001.pdf" attachment.disposition = "attachment" attachment.content_id = "Balance Sheet" return attachment @@ -95,9 +95,9 @@ def build_attachment1(): def build_attachment2(): """Build attachment mock.""" attachment = Attachment() - attachment.content = "BwdW" - attachment.type = "image/png" - attachment.filename = "banner.png" + attachment.file_content = "BwdW" + attachment.file_type = "image/png" + attachment.file_name = "banner.png" attachment.disposition = "inline" attachment.content_id = "Banner" return attachment @@ -227,19 +227,19 @@ def build_kitchen_sink(): ] message.attachment = Attachment(FileContent('base64 encoded content 1'), - FileType('application/pdf'), FileName('balance_001.pdf'), + FileType('application/pdf'), Disposition('attachment'), ContentId('Content ID 1')) message.attachment = [ Attachment(FileContent('base64 encoded content 2'), - FileType('image/png'), FileName('banner.png'), + FileType('image/png'), Disposition('inline'), ContentId('Content ID 2')), Attachment(FileContent('base64 encoded content 3'), - FileType('image/png'), FileName('banner2.png'), + FileType('image/png'), Disposition('inline'), ContentId('Content ID 3')) ] From 0c139aaeb8c188b5a06bb1c73ae80d59367b7e80 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 15 Sep 2020 15:00:28 -0500 Subject: [PATCH 008/149] docs: update the eventwebhook sample data --- test/test_eventwebhook.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/test_eventwebhook.py b/test/test_eventwebhook.py index 28f9ad282..eee1eabf9 100644 --- a/test/test_eventwebhook.py +++ b/test/test_eventwebhook.py @@ -7,14 +7,22 @@ class UnitTests(unittest.TestCase): @classmethod def setUpClass(cls): - cls.PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEDr2LjtURuePQzplybdC+u4CwrqDqBaWjcMMsTbhdbcwHBcepxo7yAQGhHPTnlvFYPAZFceEu/1FwCM/QmGUhA==' - cls.SIGNATURE = 'MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH2j/0=' - cls.TIMESTAMP = '1588788367' - cls.PAYLOAD = json.dumps({ - 'event': 'test_event', - 'category': 'example_payload', - 'message_id': 'message_id', - }, sort_keys=True, separators=(',', ':')) + cls.PUBLIC_KEY = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==' + cls.SIGNATURE = 'MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=' + cls.TIMESTAMP = '1600112502' + cls.PAYLOAD = json.dumps( + [ + { + 'email': 'hello@world.com', + 'event': 'dropped', + 'reason': 'Bounced Address', + 'sg_event_id': 'ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA', + 'sg_message_id': 'LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0', + 'smtp-id': '', + 'timestamp': 1600112492, + } + ], sort_keys=True, separators=(',', ':') + ) + '\r\n' # Be sure to include the trailing carriage return and newline! def test_verify_valid_signature(self): ew = EventWebhook() From 6980cf8eb3978aa16b21d41bcd117043d64cf057 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 16 Sep 2020 23:29:55 +0000 Subject: [PATCH 009/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d01488a7b..3c8128f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2020-09-16] Version 6.4.7 +-------------------------- +**Library - Docs** +- [PR #936](https://github.com/sendgrid/sendgrid-python/pull/936): correct attachment example. Thanks to [@Arbitrage0](https://github.com/Arbitrage0)! + + [2020-08-19] Version 6.4.6 -------------------------- **Library - Chore** From e796a3f94cfbbe6f5e94f2cb74b70e4ca3a1eef0 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 16 Sep 2020 23:34:15 +0000 Subject: [PATCH 010/149] Release 6.4.7 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index a8b77c27e..63521b92e 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.4.6' +__version__ = '6.4.7' From 397a57da6e09ce5058f79562e19d518fdd850244 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Mon, 21 Sep 2020 10:25:30 -0500 Subject: [PATCH 011/149] docs: update legacy/dynamic transactional template doc links --- use_cases/legacy_templates.md | 4 ++-- use_cases/transactional_templates.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/use_cases/legacy_templates.md b/use_cases/legacy_templates.md index c1c9204fa..c8188a257 100644 --- a/use_cases/legacy_templates.md +++ b/use_cases/legacy_templates.md @@ -1,6 +1,6 @@ # Legacy Templates -For this example, we assume you have created a [legacy template](https://sendgrid.com/docs/ui//sending-email/create-and-edit-legacy-transactional-templates). Following is the template content we used for testing. +For this example, we assume you have created a [legacy transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html) in the UI or via the API. Following is the template content we used for testing. Template ID (replace with your own): @@ -113,4 +113,4 @@ except urllib.HTTPError as e: print(response.status_code) print(response.body) print(response.headers) -``` \ No newline at end of file +``` diff --git a/use_cases/transactional_templates.md b/use_cases/transactional_templates.md index 2a237b946..9ee4848c3 100644 --- a/use_cases/transactional_templates.md +++ b/use_cases/transactional_templates.md @@ -1,6 +1,6 @@ # Transactional Templates -For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html) in the UI or via the API. Following is the template content we used for testing. +For this example, we assume you have created a [dynamic transactional template](https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/) in the UI or via the API. Following is the template content we used for testing. Email Subject: From e5253a571c1537f51be3a9e16057187cb7386653 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Wed, 14 Oct 2020 15:53:28 -0500 Subject: [PATCH 012/149] chore: fix spelling typos --- FIRST_TIMERS.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/FIRST_TIMERS.md b/FIRST_TIMERS.md index f6406f783..528580c34 100644 --- a/FIRST_TIMERS.md +++ b/FIRST_TIMERS.md @@ -61,13 +61,13 @@ Before creating a pull request, make sure that you respect the repository's cons * [Node.js SDK](https://github.com/sendgrid/sendgrid-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) * [Java SDK](https://github.com/sendgrid/sendgrid-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) * [Go SDK](https://github.com/sendgrid/sendgrid-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Python STMPAPI Client](https://github.com/sendgrid/smtpapi-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [PHP STMPAPI Client](https://github.com/sendgrid/smtpapi-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [C# STMPAPI Client](https://github.com/sendgrid/smtpapi-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Ruby STMPAPI Client](https://github.com/sendgrid/smtpapi-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Node.js STMPAPI Client](https://github.com/sendgrid/smtpapi-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Java STMPAPI Client](https://github.com/sendgrid/smtpapi-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Go STMPAPI Client](https://github.com/sendgrid/smtpapi-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Python SMTPAPI Client](https://github.com/sendgrid/smtpapi-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [PHP SMTPAPI Client](https://github.com/sendgrid/smtpapi-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [C# SMTPAPI Client](https://github.com/sendgrid/smtpapi-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Ruby SMTPAPI Client](https://github.com/sendgrid/smtpapi-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Node.js SMTPAPI Client](https://github.com/sendgrid/smtpapi-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Java SMTPAPI Client](https://github.com/sendgrid/smtpapi-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) +* [Go SMTPAPI Client](https://github.com/sendgrid/smtpapi-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) * [Python HTTP Client](https://github.com/sendgrid/python-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) * [PHP HTTP Client](https://github.com/sendgrid/php-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) * [C# HTTP Client](https://github.com/sendgrid/csharp-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) From 16497e920a9be4b01d712864180aee90ab3bf78e Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 15 Oct 2020 16:25:23 +0000 Subject: [PATCH 013/149] chore: update template files --- PULL_REQUEST_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index a86818029..46d80c650 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -19,13 +19,13 @@ Closes #2 A short description of what this PR does. ### Checklist -- [ ] I acknowledge that all my contributions will be made under the project's license +- [x] I acknowledge that all my contributions will be made under the project's license - [ ] I have made a material change to the repo (functionality, testing, spelling, grammar) -- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) and my PR follows them +- [ ] I have read the [Contribution Guidelines](https://github.com/sendgrid/sendgrid-python/blob/main/CONTRIBUTING.md) and my PR follows them - [ ] I have titled the PR appropriately - [ ] I have updated my branch with the main branch - [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have added necessary documentation about the functionality in the appropriate .md file +- [ ] I have added the necessary documentation about the functionality in the appropriate .md file - [ ] I have added inline documentation to the code I modified If you have questions, please file a [support ticket](https://twilio.com/help/contact), or create a GitHub Issue in this repository. From 9e52a66a61aeccbec7a773be1f8c28fd144994a2 Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 15 Oct 2020 16:35:17 +0000 Subject: [PATCH 014/149] chore: update template files --- PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 46d80c650..05c4d872c 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -28,4 +28,4 @@ A short description of what this PR does. - [ ] I have added the necessary documentation about the functionality in the appropriate .md file - [ ] I have added inline documentation to the code I modified -If you have questions, please file a [support ticket](https://twilio.com/help/contact), or create a GitHub Issue in this repository. +If you have questions, please file a [support ticket](https://support.sendgrid.com/hc/en-us), or create a GitHub Issue in this repository. From a453cec35d0de18b5735ff9648b62f8325da4e0e Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 15 Oct 2020 21:28:41 +0000 Subject: [PATCH 015/149] chore: update template files --- PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 05c4d872c..7c2789ae4 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -28,4 +28,4 @@ A short description of what this PR does. - [ ] I have added the necessary documentation about the functionality in the appropriate .md file - [ ] I have added inline documentation to the code I modified -If you have questions, please file a [support ticket](https://support.sendgrid.com/hc/en-us), or create a GitHub Issue in this repository. +If you have questions, please file a [support ticket](https://support.sendgrid.com), or create a GitHub Issue in this repository. From b42050cb84d6971b73f9cf35d5a996b6fdfd7201 Mon Sep 17 00:00:00 2001 From: Twilio Date: Tue, 27 Oct 2020 21:34:26 +0000 Subject: [PATCH 016/149] chore: update template files --- LICENSE.md => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE.md => LICENSE (100%) diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE From b360223622418216f89a98278cfa1cde3e2a9ceb Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 27 Oct 2020 16:42:33 -0500 Subject: [PATCH 017/149] chore: update license references --- MANIFEST.in | 2 +- README.md | 4 ++-- README.rst | 4 ++-- test/test_project.py | 4 ++-- test/test_sendgrid.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index cd3c5f680..bf5b007ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst -include LICENSE.md +include LICENSE include app.json include Procfile include requirements.txt diff --git a/README.md b/README.md index f6f8c76bc..62c79bb31 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![codecov](https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/main.svg?style=flat-square&label=Codecov+Coverage)](https://codecov.io/gh/sendgrid/sendgrid-python) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) [![Email Notifications Badge](https://dx.sendgrid.com/badge/python)](https://dx.sendgrid.com/newsletter/python) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.md) +[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) [![GitHub contributors](https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg)](https://github.com/sendgrid/sendgrid-python/graphs/contributors) [![Open Source Helpers](https://www.codetriage.com/sendgrid/sendgrid-python/badges/users.svg)](https://www.codetriage.com/sendgrid/sendgrid-python) @@ -221,4 +221,4 @@ If you've instead found a bug in the library or would like new features added, g # License -[The MIT License (MIT)](LICENSE.md) +[The MIT License (MIT)](LICENSE) diff --git a/README.rst b/README.rst index cd2c6bbac..ba131179c 100644 --- a/README.rst +++ b/README.rst @@ -286,7 +286,7 @@ License .. _Improvements to the Codebase: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#improvements-to-the-codebase .. _Review Pull Requests: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#code-reviews .. _troubleshooting guide: https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md -.. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/HEAD/LICENSE.md +.. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/HEAD/LICENSE .. |Travis Badge| image:: https://travis-ci.org/sendgrid/sendgrid-python.svg?branch=main :target: https://travis-ci.org/sendgrid/sendgrid-python @@ -301,7 +301,7 @@ License .. |Email Notifications Badge| image:: https://dx.sendgrid.com/badge/python :target: https://dx.sendgrid.com/newsletter/python .. |MIT licensed| image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: ./LICENSE.md + :target: ./LICENSE .. |Twitter Follow| image:: https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow :target: https://twitter.com/sendgrid .. |GitHub contributors| image:: https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg diff --git a/test/test_project.py b/test/test_project.py index 79f27972d..e30049a3f 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -35,9 +35,9 @@ def test_contributing(self): def test_issue_template(self): self.assertTrue(os.path.isfile('./ISSUE_TEMPLATE.md')) - # ./LICENSE.md + # ./LICENSE def test_license(self): - self.assertTrue(os.path.isfile('./LICENSE.md')) + self.assertTrue(os.path.isfile('./LICENSE')) # ./PULL_REQUEST_TEMPLATE.md def test_pr_template(self): diff --git a/test/test_sendgrid.py b/test/test_sendgrid.py index eb9ebabb3..f6177a7d5 100644 --- a/test/test_sendgrid.py +++ b/test/test_sendgrid.py @@ -2297,7 +2297,7 @@ def test_whitelabel_links__link_id__subuser_post(self): self.assertEqual(response.status_code, 200) def test_license_year(self): - LICENSE_FILE = 'LICENSE.md' + LICENSE_FILE = 'LICENSE' copyright_line = '' with open(LICENSE_FILE, 'r') as f: for line in f: From f3d78389f2b088d061eebef8d027d706378d552d Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Fri, 30 Oct 2020 14:17:14 -0700 Subject: [PATCH 018/149] chore: update badge --- README.md | 2 +- README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 62c79bb31..de23ef2f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![SendGrid Logo](twilio_sendgrid_logo.png) -[![Travis Badge](https://travis-ci.org/sendgrid/sendgrid-python.svg?branch=main)](https://travis-ci.org/sendgrid/sendgrid-python) +[![Travis Badge](https://travis-ci.com/sendgrid/sendgrid-python.svg?branch=main)](https://travis-ci.com/sendgrid/sendgrid-python) [![codecov](https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/main.svg?style=flat-square&label=Codecov+Coverage)](https://codecov.io/gh/sendgrid/sendgrid-python) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) [![Email Notifications Badge](https://dx.sendgrid.com/badge/python)](https://dx.sendgrid.com/newsletter/python) diff --git a/README.rst b/README.rst index ba131179c..a7a076c22 100644 --- a/README.rst +++ b/README.rst @@ -288,8 +288,8 @@ License .. _troubleshooting guide: https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md .. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/HEAD/LICENSE -.. |Travis Badge| image:: https://travis-ci.org/sendgrid/sendgrid-python.svg?branch=main - :target: https://travis-ci.org/sendgrid/sendgrid-python +.. |Travis Badge| image:: https://travis-ci.com/sendgrid/sendgrid-python.svg?branch=main + :target: https://travis-ci.com/sendgrid/sendgrid-python .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/sendgrid.svg :target: https://pypi.org/project/sendgrid/ .. |PyPI Version| image:: https://img.shields.io/pypi/v/sendgrid.svg From 3e40d3cc1014cfc153920bb8c489cfcbf0d3b3db Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Mon, 30 Nov 2020 21:57:12 +0200 Subject: [PATCH 019/149] docs: fixed typo in sendgrid/helpers/mail/file_content.py (#955) --- sendgrid/helpers/mail/file_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/helpers/mail/file_content.py b/sendgrid/helpers/mail/file_content.py index c5c0d6995..c1eb81fc6 100644 --- a/sendgrid/helpers/mail/file_content.py +++ b/sendgrid/helpers/mail/file_content.py @@ -31,7 +31,7 @@ def file_content(self, value): def get(self): """ - Get a JSON-ready representation of this FileContente. + Get a JSON-ready representation of this FileContent. :returns: This FileContent, ready for use in a request body. :rtype: string From 841088f0ef63c50d3ed51a74e82a172151a7016e Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 2 Dec 2020 19:55:02 +0000 Subject: [PATCH 020/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8128f10..275cdcbc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2020-12-02] Version 6.4.8 +-------------------------- +**Library - Docs** +- [PR #955](https://github.com/sendgrid/sendgrid-python/pull/955): fixed typo in sendgrid/helpers/mail/file_content.py. Thanks to [@razvandimescu](https://github.com/razvandimescu)! + + [2020-09-16] Version 6.4.7 -------------------------- **Library - Docs** From 62315e699555fb78173edf2e2acbb510eab9aa18 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 2 Dec 2020 19:59:43 +0000 Subject: [PATCH 021/149] Release 6.4.8 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 63521b92e..bec64b5d2 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.4.7' +__version__ = '6.4.8' From 74293fd053e7d8da87e7714dba009aa3db3602d5 Mon Sep 17 00:00:00 2001 From: Michael Kennedy Date: Mon, 28 Dec 2020 13:40:53 -0800 Subject: [PATCH 022/149] docs: Sending HTML email example is broken (#962) --- use_cases/sending_html_content.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/use_cases/sending_html_content.md b/use_cases/sending_html_content.md index ba38b19aa..4a828e737 100644 --- a/use_cases/sending_html_content.md +++ b/use_cases/sending_html_content.md @@ -35,14 +35,13 @@ html_text = """ sendgrid_client = SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) from_email = From("from_email@exmaple.com") -to_email = Email("to_email@example.com") +to_email = To("to_email@example.com") subject = Subject("Test Subject") html_content = HtmlContent(html_text) soup = BeautifulSoup(html_text) plain_text = soup.get_text() plain_text_content = Content("text/plain", plain_text) -mail.add_content(plain_content) message = Mail(from_email, to_email, subject, plain_text_content, html_content) @@ -54,4 +53,4 @@ try: except urllib.HTTPError as e: print(e.read()) exit() -``` \ No newline at end of file +``` From 6807622981ec37abb05c8c247a6a6fbbab2a0f9f Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 6 Jan 2021 18:18:37 +0000 Subject: [PATCH 023/149] chore: update template files --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 29aba592a..e5439a92d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2020, Twilio SendGrid, Inc. +Copyright (C) 2021, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 39844d63dd9023adaee5da79b372b1aee8fbee0a Mon Sep 17 00:00:00 2001 From: arun tvs Date: Sat, 9 Jan 2021 03:31:27 +0530 Subject: [PATCH 024/149] feat: Support for AMP HTML Email (#945) --- sendgrid/helpers/mail/__init__.py | 1 + sendgrid/helpers/mail/amp_html_content.py | 59 +++++ sendgrid/helpers/mail/content.py | 6 +- sendgrid/helpers/mail/mail.py | 23 +- sendgrid/helpers/mail/mime_type.py | 1 + test/test_mail_helpers.py | 267 ++++++++++++++++++++++ use_cases/sending_amp_html_content.md | 102 +++++++++ 7 files changed, 454 insertions(+), 5 deletions(-) create mode 100644 sendgrid/helpers/mail/amp_html_content.py create mode 100644 use_cases/sending_amp_html_content.md diff --git a/sendgrid/helpers/mail/__init__.py b/sendgrid/helpers/mail/__init__.py index 15cc1cc7e..28d80ac18 100644 --- a/sendgrid/helpers/mail/__init__.py +++ b/sendgrid/helpers/mail/__init__.py @@ -27,6 +27,7 @@ from .groups_to_display import GroupsToDisplay from .header import Header from .html_content import HtmlContent +from .amp_html_content import AmpHtmlContent from .ip_pool_name import IpPoolName from .mail_settings import MailSettings from .mail import Mail diff --git a/sendgrid/helpers/mail/amp_html_content.py b/sendgrid/helpers/mail/amp_html_content.py new file mode 100644 index 000000000..1a282053f --- /dev/null +++ b/sendgrid/helpers/mail/amp_html_content.py @@ -0,0 +1,59 @@ +from .content import Content +from .validators import ValidateApiKey + + +class AmpHtmlContent(Content): + """AMP HTML content to be included in your email.""" + + def __init__(self, content): + """Create an AMP HTML Content with the specified MIME type and content. + + :param content: The AMP HTML content. + :type content: string + """ + self._content = None + self._validator = ValidateApiKey() + + if content is not None: + self.content = content + + @property + def mime_type(self): + """The MIME type for AMP HTML content. + + :rtype: string + """ + return "text/x-amp-html" + + @property + def content(self): + """The actual AMP HTML content. + + :rtype: string + """ + return self._content + + @content.setter + def content(self, value): + """The actual AMP HTML content. + + :param value: The actual AMP HTML content. + :type value: string + """ + self._validator.validate_message_dict(value) + self._content = value + + def get(self): + """ + Get a JSON-ready representation of this AmpContent. + + :returns: This AmpContent, ready for use in a request body. + :rtype: dict + """ + content = {} + if self.mime_type is not None: + content["type"] = self.mime_type + + if self.content is not None: + content["value"] = self.content + return content diff --git a/sendgrid/helpers/mail/content.py b/sendgrid/helpers/mail/content.py index 73bf9f64d..618eee917 100644 --- a/sendgrid/helpers/mail/content.py +++ b/sendgrid/helpers/mail/content.py @@ -29,7 +29,7 @@ def __init__(self, mime_type, content): @property def mime_type(self): """The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :rtype: string """ @@ -38,11 +38,11 @@ def mime_type(self): @mime_type.setter def mime_type(self, value): """The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :param value: The MIME type of the content you are including in your email. - For example, "text/plain" or "text/html". + For example, "text/plain" or "text/html" or "text/x-amp-html". :type value: string """ self._mime_type = value diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index db2399310..0069a3f7d 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -27,6 +27,7 @@ def __init__( subject=None, plain_text_content=None, html_content=None, + amp_html_content=None, global_substitutions=None, is_multiple=False): """ @@ -43,6 +44,8 @@ def __init__( :type plain_text_content: string, optional :param html_content: The html body of the email :type html_content: string, optional + :param amp_html_content: The amp-html body of the email + :type amp_html_content: string, optional """ self._attachments = None self._categories = None @@ -71,6 +74,8 @@ def __init__( self.subject = subject if plain_text_content is not None: self.add_content(plain_text_content, MimeType.text) + if amp_html_content is not None: + self.add_content(amp_html_content, MimeType.amp) if html_content is not None: self.add_content(html_content, MimeType.html) @@ -725,9 +730,23 @@ def add_content(self, content, mime_type=None): """ if isinstance(content, str): content = Content(mime_type, content) - # Content of mime type text/plain must always come first - if content.mime_type == "text/plain": + # Content of mime type text/plain must always come first, followed by text/x-amp-html and then text/html + if content.mime_type == MimeType.text: self._contents = self._ensure_insert(content, self._contents) + elif content.mime_type == MimeType.amp: + if self._contents: + for _content in self._contents: + # this is written in the context that plain text content will always come earlier than the html content + if _content.mime_type == MimeType.text: + index = 1 + break + elif _content.mime_type == MimeType.html: + index = 0 + break + else: + index = 0 + self._contents = self._ensure_append( + content, self._contents, index=index) else: if self._contents: index = len(self._contents) diff --git a/sendgrid/helpers/mail/mime_type.py b/sendgrid/helpers/mail/mime_type.py index 0d0c9b3b3..a2f88c5af 100644 --- a/sendgrid/helpers/mail/mime_type.py +++ b/sendgrid/helpers/mail/mime_type.py @@ -3,3 +3,4 @@ class MimeType(object): """ text = "text/plain" html = "text/html" + amp = "text/x-amp-html" diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index 202d3948b..d0d09c9d3 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -17,6 +17,40 @@ Subject, Substitution, To, Cc, Bcc, TrackingSettings ) +# The below amp html email content is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/) +amp_html_content = '''

Hello!

''' + +response_content_with_all_three_mime_contents = json.dumps({ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/x-amp-html", + "value": amp_html_content + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" +}) class UnitTests(unittest.TestCase): @@ -284,6 +318,239 @@ def test_multiple_emails_to_multiple_recipients(self): }''') ) + def test_single_email_with_all_three_email_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + amp_html_content=AmpHtmlContent(amp_html_content), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python') + ) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_amp_and_html_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + amp_html_content=AmpHtmlContent(amp_html_content), + html_content=HtmlContent( + 'and easy to do anywhere, even with Python') + ) + + response_content = json.dumps({ + "content": [ + { + "type": "text/x-amp-html", + "value": amp_html_content + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" + }) + self.assertEqual( + message.get(), + json.loads(response_content) + ) + + def test_single_email_with_amp_and_plain_contents_to_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun'), + plain_text_content=PlainTextContent( + 'and easy to do anywhere, even with Python'), + amp_html_content=AmpHtmlContent(amp_html_content) + ) + + response_content = json.dumps({ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/x-amp-html", + "value": amp_html_content + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to@example.com", + "name": "Example To Name" + } + ] + } + ], + "subject": "Sending with SendGrid is Fun" + }) + self.assertEqual( + message.get(), + json.loads(response_content) + ) + + ## Check ordering of MIME types in different variants - start + def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_amp_html_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_plain_html_amp_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_html_plain_amp_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_html_amp_plain_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = AmpHtmlContent(amp_html_content) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_html_plain_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = AmpHtmlContent(amp_html_content) + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + def test_single_email_with_all_three_contents_in_collapsed_order_of_amp_plain_html_content_single_recipient(self): + from sendgrid.helpers.mail import (Mail, From, To, Subject, + PlainTextContent, HtmlContent, AmpHtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to@example.com', 'Example To Name'), + subject=Subject('Sending with SendGrid is Fun') + ) + message.content = AmpHtmlContent(amp_html_content) + message.content = PlainTextContent( + 'and easy to do anywhere, even with Python') + message.content = HtmlContent( + 'and easy to do anywhere, even with Python') + + self.assertEqual( + message.get(), + json.loads(response_content_with_all_three_mime_contents) + ) + + ## end + def test_value_error_is_raised_on_to_emails_set_to_list_of_lists(self): from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) self.maxDiff = None diff --git a/use_cases/sending_amp_html_content.md b/use_cases/sending_amp_html_content.md new file mode 100644 index 000000000..616b52039 --- /dev/null +++ b/use_cases/sending_amp_html_content.md @@ -0,0 +1,102 @@ +# Sending AMP-HTML Email + +Following is an example on how to send an AMP HTML Email. +Currently, we require AMP HTML and any one of HTML or Plain Text content (preferrably both) for improved deliverability or fallback for AMP HTML Email for supporting older clients and showing alternate content after 30 days. + +For more information on AMP emails pls check the [official AMP email page](https://amp.dev/about/email/) + +```python +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail + +# The below amp html email is taken from [Google AMP Hello World Email](https://amp.dev/documentation/examples/introduction/hello_world_email/) +amp_html_content = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Hello!

+ + + + + + + + +
+ + +''' + +message = Mail( + from_email='example@example.com', + to_emails='example@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python', + amp_html_content=amp_html_content) +try: + sg = SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) + response = sg.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e.message) +``` \ No newline at end of file From 92ad033da0a7f843eb138469b026ef3578ff62ca Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 13 Jan 2021 20:20:17 +0000 Subject: [PATCH 025/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 275cdcbc9..eba273953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-01-13] Version 6.5.0 +-------------------------- +**Library - Feature** +- [PR #945](https://github.com/sendgrid/sendgrid-python/pull/945): Support for AMP HTML Email. Thanks to [@modernwarfareuplink](https://github.com/modernwarfareuplink)! + +**Library - Docs** +- [PR #962](https://github.com/sendgrid/sendgrid-python/pull/962): Sending HTML email example is broken. Thanks to [@mikeckennedy](https://github.com/mikeckennedy)! + + [2020-12-02] Version 6.4.8 -------------------------- **Library - Docs** From bea36da8ecd535f22d266a3a7f2bd6377af7d7cf Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 13 Jan 2021 20:24:49 +0000 Subject: [PATCH 026/149] Release 6.5.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index bec64b5d2..4a09e5e87 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.4.8' +__version__ = '6.5.0' From fdf5c497767382879134175e21867c851905e792 Mon Sep 17 00:00:00 2001 From: John Calhoun Date: Fri, 29 Jan 2021 15:48:09 -0800 Subject: [PATCH 027/149] feat: remove duplicate emails ignoring case in Personalization (#924) Co-authored-by: childish-sambino Co-authored-by: Elmer Thomas Co-authored-by: Elise Shanholtz --- sendgrid/helpers/mail/personalization.py | 20 ++- test/test_mail_helpers.py | 218 +++++++++++++++++++++++ 2 files changed, 235 insertions(+), 3 deletions(-) diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 9239f9458..21a31c863 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -27,6 +27,20 @@ def add_email(self, email): self.add_bcc(email) return raise ValueError('Please use a To, Cc or Bcc object.') + + def _get_unique_recipients(self, recipients): + unique_recipients = [] + + for recipient in recipients: + recipient_email = recipient['email'].lower() if isinstance(recipient, dict) else recipient.email.lower() + if all( + unique_recipient['email'].lower() != recipient_email for unique_recipient in unique_recipients + ): + new_unique_recipient = recipient if isinstance(recipient, dict) else recipient.get() + unique_recipients.append(new_unique_recipient) + + return unique_recipients + @property def tos(self): @@ -34,7 +48,7 @@ def tos(self): :rtype: list(dict) """ - return self._tos + return self._get_unique_recipients(self._tos) @tos.setter def tos(self, value): @@ -69,7 +83,7 @@ def ccs(self): :rtype: list(dict) """ - return self._ccs + return self._get_unique_recipients(self._ccs) @ccs.setter def ccs(self, value): @@ -89,7 +103,7 @@ def bccs(self): :rtype: list(dict) """ - return self._bccs + return self._get_unique_recipients(self._bccs) @bccs.setter def bccs(self, value): diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index d0d09c9d3..752a9fd85 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -646,6 +646,224 @@ def test_error_is_not_raised_on_to_emails_includes_bcc_cc(self): html_content=HtmlContent( 'and easy to do anywhere, even with Python')) + def test_personalization_add_email_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + p.add_email(to_email) + p.add_email(to_email) + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_add_email_filters_out_duplicate_to_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + to_email_with_caps = To('test+TO0@example.com', 'Example To Name 0') + p.add_email(to_email) + p.add_email(to_email_with_caps) + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + p.add_email(cc_email) + p.add_email(cc_email) + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_filters_out_duplicate_cc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + cc_email_with_caps = Cc('test+CC0@example.com', 'Example Cc Name 0') + p.add_email(cc_email) + p.add_email(cc_email_with_caps) + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + p.add_email(bcc_email) + p.add_email(bcc_email) + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_filters_out_duplicate_bcc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + bcc_email_with_caps = Bcc('test+BCC0@example.com', 'Example Bcc Name 0') + p.add_email(bcc_email) + p.add_email(bcc_email_with_caps) + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_tos_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + to_emails = [{ 'email': 'test+to0@example.com', 'name': 'Example To Name 0' }] * 2 + p.tos = to_emails + + self.assertEqual([to_emails[0]], p.tos) + + def test_personalization_tos_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = { 'email': 'test+to0@example.com', 'name': 'Example To Name 0' } + to_email_with_caps = { 'email': 'test+TO0@example.com', 'name': 'Example To Name 0' } + to_emails = [to_email, to_email_with_caps] + p.tos = to_emails + + self.assertEqual([to_email], p.tos) + + def test_personalization_tos_setter_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_emails = [To('test+to0@example.com', 'Example To Name 0')] * 2 + p.tos = to_emails + + self.assertEqual([to_emails[0].get()], p.tos) + + + def test_personalization_tos_setter_filters_out_duplicate_to_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + to_email_with_caps = To('test+TO0@example.com', 'Example To Name 0') + to_emails = [to_email, to_email_with_caps] + p.tos = to_emails + + self.assertEqual([to_email.get()], p.tos) + + def test_personalization_ccs_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + cc_emails = [{ 'email': 'test+cc0@example.com', 'name': 'Example Cc Name 0' }] * 2 + p.ccs = cc_emails + + self.assertEqual([cc_emails[0]], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = { 'email': 'test+cc0@example.com', 'name': 'Example Cc Name 0' } + cc_email_with_caps = { 'email': 'test+CC0@example.com', 'name': 'Example Cc Name 0' } + cc_emails = [cc_email, cc_email_with_caps] + p.ccs = cc_emails + + self.assertEqual([cc_email], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_emails = [Cc('test+cc0@example.com', 'Example Cc Name 0')] * 2 + p.ccs = cc_emails + + self.assertEqual([cc_emails[0].get()], p.ccs) + + def test_personalization_ccs_setter_filters_out_duplicate_cc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+cc0@example.com', 'Example Cc Name 0') + cc_email_with_caps = Cc('test+CC0@example.com', 'Example Cc Name 0') + p.ccs = [cc_email, cc_email_with_caps] + + self.assertEqual([cc_email.get()], p.ccs) + + def test_personalization_bccs_setter_filters_out_duplicate_dict_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_emails = [{ 'email': 'test+bcc0@example.com', 'name': 'Example Bcc Name 0' }] * 2 + p.bccs = bcc_emails + + self.assertEqual([bcc_emails[0]], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_dict_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = { 'email': 'test+bcc0@example.com', 'name': 'Example Bcc Name 0' } + bcc_email_with_caps = { 'email': 'test+BCC0@example.com', 'name': 'Example Bcc Name 0' } + bcc_emails = [bcc_email, bcc_email_with_caps] + p.bccs = bcc_emails + + self.assertEqual([bcc_email], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_emails = [Bcc('test+bcc0@example.com', 'Example Bcc Name 0')] * 2 + p.bccs = bcc_emails + + self.assertEqual([bcc_emails[0].get()], p.bccs) + + def test_personalization_bccs_setter_filters_out_duplicate_bcc_emails_ignoring_case(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+bcc0@example.com', 'Example Bcc Name 0') + bcc_email_with_caps = Bcc('test+BCC0@example.com', 'Example Bcc Name 0') + p.bccs = [bcc_email, bcc_email_with_caps] + + self.assertEqual([bcc_email.get()], p.bccs) + + def test_personalization_add_to_filters_out_duplicate_to_emails(self): + self.maxDiff = None + + p = Personalization() + to_email = To('test+to0@example.com', 'Example To Name 0') + p.add_to(to_email) + p.add_to(to_email) + + expected = [to_email.get()] + + self.assertEqual(expected, p.tos) + + def test_personalization_add_bcc_filters_out_duplicate_bcc_emails(self): + self.maxDiff = None + + p = Personalization() + bcc_email = Bcc('test+to0@example.com', 'Example To Name 0') + p.add_bcc(bcc_email) + p.add_bcc(bcc_email) + + expected = [bcc_email.get()] + + self.assertEqual(expected, p.bccs) + + def test_personalization_add_cc_filters_out_duplicate_cc_emails(self): + self.maxDiff = None + + p = Personalization() + cc_email = Cc('test+to0@example.com', 'Example To Name 0') + p.add_cc(cc_email) + p.add_cc(cc_email) + + expected = [cc_email.get()] + + self.assertEqual(expected, p.ccs) + def test_dynamic_template_data(self): self.maxDiff = None From eb3014bdcb3de287c6155f37502a5fb80ed01716 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Sat, 6 Feb 2021 06:57:42 +0530 Subject: [PATCH 028/149] docs: Use correct pip installation command (#964) --- TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 589aa2e6a..b9a68b8c0 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -72,7 +72,7 @@ Using pip: ```bash pip uninstall sendgrid -pip install sendgrid=1.6.22 +pip install sendgrid==1.6.22 ``` Download: From 3f97a7fed7b48d7cbe3b80db81abf5bb170bf102 Mon Sep 17 00:00:00 2001 From: Ben Lopatin Date: Fri, 5 Feb 2021 20:29:19 -0500 Subject: [PATCH 029/149] fix: replace names in BatchId docstrings (#971) --- sendgrid/helpers/mail/batch_id.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sendgrid/helpers/mail/batch_id.py b/sendgrid/helpers/mail/batch_id.py index de9960ca2..a4c0f8e9d 100644 --- a/sendgrid/helpers/mail/batch_id.py +++ b/sendgrid/helpers/mail/batch_id.py @@ -18,7 +18,7 @@ def __init__(self, batch_id=None): @property def batch_id(self): - """A unix timestamp. + """The batch ID. :rtype: string """ @@ -26,7 +26,7 @@ def batch_id(self): @batch_id.setter def batch_id(self, value): - """A unix timestamp. + """The batch ID. :param value: Batch Id :type value: string @@ -42,7 +42,7 @@ def __str__(self): def get(self): """ - Get a JSON-ready representation of this SendAt object. + Get a JSON-ready representation of this BatchId object. :returns: The BatchId, ready for use in a request body. :rtype: string From 7821d42f683b44e51c7285c9dc47ee94fe3ee8f4 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 10 Feb 2021 22:56:08 +0000 Subject: [PATCH 030/149] [Librarian] Version Bump --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eba273953..b9344367e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-02-10] Version 6.6.0 +-------------------------- +**Library - Docs** +- [PR #964](https://github.com/sendgrid/sendgrid-python/pull/964): Use correct pip installation command. Thanks to [@Akasurde](https://github.com/Akasurde)! + +**Library - Fix** +- [PR #971](https://github.com/sendgrid/sendgrid-python/pull/971): replace names in BatchId docstrings. Thanks to [@bennylope](https://github.com/bennylope)! + +**Library - Feature** +- [PR #924](https://github.com/sendgrid/sendgrid-python/pull/924): remove duplicate emails ignoring case in Personalization. Thanks to [@DougCal](https://github.com/DougCal)! + + [2021-01-13] Version 6.5.0 -------------------------- **Library - Feature** From 4606b8557eb30e5972dfcf1b2e00498c75179ae5 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 10 Feb 2021 23:13:00 +0000 Subject: [PATCH 031/149] Release 6.6.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 4a09e5e87..ce16fc2fb 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.5.0' +__version__ = '6.6.0' From 423f2d07766f59507c3c19e289ac346ad6978ba7 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Mon, 8 Mar 2021 23:24:31 -0800 Subject: [PATCH 032/149] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index de23ef2f6..880589c81 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ Please see [our helper](sendgrid/helpers/inbound) for utilizing our Inbound Pars # Announcements +Our Developer Experience team is conducting planned maintenance from 03/09/2021 until 03/11/2021. Our next release is scheduled for 03/15/2021. + Please see our announcement regarding [breaking changes](https://github.com/sendgrid/sendgrid-python/issues/217). Your support is appreciated! All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). You may also subscribe to email [release notifications](https://dx.sendgrid.com/newsletter/java) for releases and breaking changes. From 3e718fb689d75bfc6c509aa88d7f39214e8bed91 Mon Sep 17 00:00:00 2001 From: Twilio Date: Fri, 12 Mar 2021 20:35:37 +0000 Subject: [PATCH 033/149] chore: update template files --- .github/ISSUE_TEMPLATE/config.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..afcba3446 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +contact_links: + - name: Twilio SendGrid Support + url: https://support.sendgrid.com + about: Get Support + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/sendgrid-python+or+sendgrid+python + about: Ask questions on Stack Overflow + - name: Documentation + url: https://sendgrid.com/docs/for-developers/ + about: View Reference Documentation From 38b0ad658a369694471e62808b4e7e3b80520899 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Mon, 15 Mar 2021 12:48:21 -0700 Subject: [PATCH 034/149] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 880589c81..de23ef2f6 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,6 @@ Please see [our helper](sendgrid/helpers/inbound) for utilizing our Inbound Pars # Announcements -Our Developer Experience team is conducting planned maintenance from 03/09/2021 until 03/11/2021. Our next release is scheduled for 03/15/2021. - Please see our announcement regarding [breaking changes](https://github.com/sendgrid/sendgrid-python/issues/217). Your support is appreciated! All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). You may also subscribe to email [release notifications](https://dx.sendgrid.com/newsletter/java) for releases and breaking changes. From 16eb0c33c97f8ca293fbc0d8965afd87c0d2f99a Mon Sep 17 00:00:00 2001 From: Alex Narayanan <67122654+anarayanan604@users.noreply.github.com> Date: Fri, 9 Apr 2021 19:02:50 -0400 Subject: [PATCH 035/149] feat: add v3 bypass filters (#983) --- sendgrid/helpers/mail/__init__.py | 3 + .../helpers/mail/bypass_bounce_management.py | 48 ++++++++++ .../helpers/mail/bypass_spam_management.py | 47 ++++++++++ .../mail/bypass_unsubscribe_management.py | 49 +++++++++++ sendgrid/helpers/mail/mail_settings.py | 87 +++++++++++++++++++ test/test_mail_helpers.py | 54 +++++++++++- use_cases/kitchen_sink.md | 6 +- 7 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 sendgrid/helpers/mail/bypass_bounce_management.py create mode 100644 sendgrid/helpers/mail/bypass_spam_management.py create mode 100644 sendgrid/helpers/mail/bypass_unsubscribe_management.py diff --git a/sendgrid/helpers/mail/__init__.py b/sendgrid/helpers/mail/__init__.py index 28d80ac18..358f2d912 100644 --- a/sendgrid/helpers/mail/__init__.py +++ b/sendgrid/helpers/mail/__init__.py @@ -4,7 +4,10 @@ from .bcc_email import Bcc from .bcc_settings import BccSettings from .bcc_settings_email import BccSettingsEmail +from .bypass_bounce_management import BypassBounceManagement from .bypass_list_management import BypassListManagement +from .bypass_spam_management import BypassSpamManagement +from .bypass_unsubscribe_management import BypassUnsubscribeManagement from .category import Category from .cc_email import Cc from .click_tracking import ClickTracking diff --git a/sendgrid/helpers/mail/bypass_bounce_management.py b/sendgrid/helpers/mail/bypass_bounce_management.py new file mode 100644 index 000000000..b0a35105c --- /dev/null +++ b/sendgrid/helpers/mail/bypass_bounce_management.py @@ -0,0 +1,48 @@ +class BypassBounceManagement(object): + """Setting for Bypass Bounce Management + + + Allows you to bypass the bounce list to ensure that the email is delivered to recipients. + Spam report and unsubscribe lists will still be checked; addresses on these other lists + will not receive the message. This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassBounceManagement. + + :param enable: Whether emails should bypass bounce management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassBounceManagement. + + :returns: This BypassBounceManagement, ready for use in a request body. + :rtype: dict + """ + bypass_bounce_management = {} + if self.enable is not None: + bypass_bounce_management["enable"] = self.enable + return bypass_bounce_management diff --git a/sendgrid/helpers/mail/bypass_spam_management.py b/sendgrid/helpers/mail/bypass_spam_management.py new file mode 100644 index 000000000..9b2552eb9 --- /dev/null +++ b/sendgrid/helpers/mail/bypass_spam_management.py @@ -0,0 +1,47 @@ +class BypassSpamManagement(object): + """Setting for Bypass Spam Management + + Allows you to bypass the spam report list to ensure that the email is delivered to recipients. + Bounce and unsubscribe lists will still be checked; addresses on these other lists will not + receive the message. This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassSpamManagement. + + :param enable: Whether emails should bypass spam management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassSpamManagement. + + :returns: This BypassSpamManagement, ready for use in a request body. + :rtype: dict + """ + bypass_spam_management = {} + if self.enable is not None: + bypass_spam_management["enable"] = self.enable + return bypass_spam_management diff --git a/sendgrid/helpers/mail/bypass_unsubscribe_management.py b/sendgrid/helpers/mail/bypass_unsubscribe_management.py new file mode 100644 index 000000000..4867fac22 --- /dev/null +++ b/sendgrid/helpers/mail/bypass_unsubscribe_management.py @@ -0,0 +1,49 @@ +class BypassUnsubscribeManagement(object): + """Setting for Bypass Unsubscribe Management + + + Allows you to bypass the global unsubscribe list to ensure that the email is delivered to recipients. + Bounce and spam report lists will still be checked; addresses on these other lists will not receive + the message. This filter applies only to global unsubscribes and will not bypass group unsubscribes. + This filter cannot be combined with the bypass_list_management filter. + """ + + def __init__(self, enable=None): + """Create a BypassUnsubscribeManagement. + + :param enable: Whether emails should bypass unsubscribe management. + :type enable: boolean, optional + """ + self._enable = None + + if enable is not None: + self.enable = enable + + @property + def enable(self): + """Indicates if this setting is enabled. + + :rtype: boolean + """ + return self._enable + + @enable.setter + def enable(self, value): + """Indicates if this setting is enabled. + + :param value: Indicates if this setting is enabled. + :type value: boolean + """ + self._enable = value + + def get(self): + """ + Get a JSON-ready representation of this BypassUnsubscribeManagement. + + :returns: This BypassUnsubscribeManagement, ready for use in a request body. + :rtype: dict + """ + bypass_unsubscribe_management = {} + if self.enable is not None: + bypass_unsubscribe_management["enable"] = self.enable + return bypass_unsubscribe_management diff --git a/sendgrid/helpers/mail/mail_settings.py b/sendgrid/helpers/mail/mail_settings.py index 45b7db77f..78499ac30 100644 --- a/sendgrid/helpers/mail/mail_settings.py +++ b/sendgrid/helpers/mail/mail_settings.py @@ -3,7 +3,10 @@ class MailSettings(object): def __init__(self, bcc_settings=None, + bypass_bounce_management=None, bypass_list_management=None, + bypass_spam_management=None, + bypass_unsubscribe_management=None, footer_settings=None, sandbox_mode=None, spam_check=None): @@ -11,9 +14,18 @@ def __init__(self, :param bcc_settings: The BCC Settings of this MailSettings :type bcc_settings: BCCSettings, optional + :param bypass_bounce_management: Whether this MailSettings bypasses bounce management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassBounceManagement, optional :param bypass_list_management: Whether this MailSettings bypasses list management :type bypass_list_management: BypassListManagement, optional + :param bypass_spam_management: Whether this MailSettings bypasses spam management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassSpamManagement, optional + :param bypass_unsubscribe_management: Whether this MailSettings bypasses unsubscribe management. + Should not be combined with bypass_list_management. + :type bypass_list_management: BypassUnsubscribeManagement, optional :param footer_settings: The default footer specified by this MailSettings :type footer_settings: FooterSettings, optional @@ -24,7 +36,10 @@ def __init__(self, :type spam_check: SpamCheck, optional """ self._bcc_settings = None + self._bypass_bounce_management = None self._bypass_list_management = None + self._bypass_spam_management = None + self._bypass_unsubscribe_management = None self._footer_settings = None self._sandbox_mode = None self._spam_check = None @@ -32,9 +47,18 @@ def __init__(self, if bcc_settings is not None: self.bcc_settings = bcc_settings + if bypass_bounce_management is not None: + self.bypass_bounce_management = bypass_bounce_management + if bypass_list_management is not None: self.bypass_list_management = bypass_list_management + if bypass_spam_management is not None: + self.bypass_spam_management = bypass_spam_management + + if bypass_unsubscribe_management is not None: + self.bypass_unsubscribe_management = bypass_unsubscribe_management + if footer_settings is not None: self.footer_settings = footer_settings @@ -61,6 +85,23 @@ def bcc_settings(self, value): """ self._bcc_settings = value + @property + def bypass_bounce_management(self): + """Whether this MailSettings bypasses bounce management. + + :rtype: BypassBounceManagement + """ + return self._bypass_bounce_management + + @bypass_bounce_management.setter + def bypass_bounce_management(self, value): + """Whether this MailSettings bypasses bounce management. + + :param value: Whether this MailSettings bypasses bounce management. + :type value: BypassBounceManagement + """ + self._bypass_bounce_management = value + @property def bypass_list_management(self): """Whether this MailSettings bypasses list management. @@ -78,6 +119,40 @@ def bypass_list_management(self, value): """ self._bypass_list_management = value + @property + def bypass_spam_management(self): + """Whether this MailSettings bypasses spam management. + + :rtype: BypassSpamManagement + """ + return self._bypass_spam_management + + @bypass_spam_management.setter + def bypass_spam_management(self, value): + """Whether this MailSettings bypasses spam management. + + :param value: Whether this MailSettings bypasses spam management. + :type value: BypassSpamManagement + """ + self._bypass_spam_management = value + + @property + def bypass_unsubscribe_management(self): + """Whether this MailSettings bypasses unsubscribe management. + + :rtype: BypassUnsubscribeManagement + """ + return self._bypass_unsubscribe_management + + @bypass_unsubscribe_management.setter + def bypass_unsubscribe_management(self, value): + """Whether this MailSettings bypasses unsubscribe management. + + :param value: Whether this MailSettings bypasses unsubscribe management. + :type value: BypassUnsubscribeManagement + """ + self._bypass_unsubscribe_management = value + @property def footer_settings(self): """The default footer specified by this MailSettings. @@ -141,10 +216,22 @@ def get(self): if self.bcc_settings is not None: mail_settings["bcc"] = self.bcc_settings.get() + if self.bypass_bounce_management is not None: + mail_settings[ + "bypass_bounce_management"] = self.bypass_bounce_management.get() + if self.bypass_list_management is not None: mail_settings[ "bypass_list_management"] = self.bypass_list_management.get() + if self.bypass_spam_management is not None: + mail_settings[ + "bypass_spam_management"] = self.bypass_spam_management.get() + + if self.bypass_unsubscribe_management is not None: + mail_settings[ + "bypass_unsubscribe_management"] = self.bypass_unsubscribe_management.get() + if self.footer_settings is not None: mail_settings["footer"] = self.footer_settings.get() diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index 752a9fd85..57a4ba880 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -935,7 +935,8 @@ def test_kitchen_sink(self): FileContent, FileType, Disposition, ContentId, TemplateId, Section, ReplyTo, Category, BatchId, Asm, GroupId, GroupsToDisplay, IpPoolName, MailSettings, BccSettings, BccSettingsEmail, - BypassListManagement, FooterSettings, FooterText, + BypassBounceManagement, BypassListManagement, BypassSpamManagement, + BypassUnsubscribeManagement, FooterSettings, FooterText, FooterHtml, SandBoxMode, SpamCheck, SpamThreshold, SpamUrl, TrackingSettings, ClickTracking, SubscriptionTracking, SubscriptionText, SubscriptionHtml, SubscriptionSubstitutionTag, @@ -1116,7 +1117,10 @@ def test_kitchen_sink(self): mail_settings = MailSettings() mail_settings.bcc_settings = BccSettings( False, BccSettingsEmail("bcc@twilio.com")) + mail_settings.bypass_bounce_management = BypassBounceManagement(False) mail_settings.bypass_list_management = BypassListManagement(False) + mail_settings.bypass_spam_management = BypassSpamManagement(False) + mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(False) mail_settings.footer_settings = FooterSettings( True, FooterText("w00t"), FooterHtml("w00t!")) mail_settings.sandbox_mode = SandBoxMode(True) @@ -1223,9 +1227,18 @@ def test_kitchen_sink(self): "email": "bcc@twilio.com", "enable": false }, + "bypass_bounce_management": { + "enable": false + }, "bypass_list_management": { "enable": false }, + "bypass_spam_management": { + "enable": false + }, + "bypass_unsubscribe_management": { + "enable": false + }, "footer": { "enable": true, "html": "w00t!", @@ -1613,3 +1626,42 @@ def test_disable_tracking(self): tracking_settings.get(), {'click_tracking': {'enable': False, 'enable_text': False}} ) + + def test_bypass_list_management(self): + from sendgrid.helpers.mail import (MailSettings, BypassListManagement) + mail_settings = MailSettings() + mail_settings.bypass_list_management = BypassListManagement(True) + + self.assertEqual( + mail_settings.get(), + { + "bypass_list_management": { + "enable": True + }, + }, + ) + + def test_v3_bypass_filters(self): + from sendgrid.helpers.mail import ( + MailSettings, BypassBounceManagement, + BypassSpamManagement, BypassUnsubscribeManagement + ) + mail_settings = MailSettings() + mail_settings.bypass_bounce_management = BypassBounceManagement(True) + mail_settings.bypass_spam_management = BypassSpamManagement(True) + mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(True) + + self.assertEqual( + mail_settings.get(), + { + "bypass_bounce_management": { + "enable": True + }, + "bypass_spam_management": { + "enable": True + }, + "bypass_unsubscribe_management": { + "enable": True + }, + }, + ) diff --git a/use_cases/kitchen_sink.md b/use_cases/kitchen_sink.md index 68c9e057e..c0a301117 100644 --- a/use_cases/kitchen_sink.md +++ b/use_cases/kitchen_sink.md @@ -8,7 +8,8 @@ from sendgrid.helpers.mail import ( FileContent, FileType, Disposition, ContentId, TemplateId, Section, ReplyTo, Category, BatchId, Asm, GroupId, GroupsToDisplay, IpPoolName, MailSettings, BccSettings, BccSettingsEmail, - BypassListManagement, FooterSettings, FooterText, + BypassBounceManagement, BypassListManagement, BypassSpamManagement, + BypassUnsubscribeManagement, FooterSettings, FooterText, FooterHtml, SandBoxMode, SpamCheck, SpamThreshold, SpamUrl, TrackingSettings, ClickTracking, SubscriptionTracking, SubscriptionText, SubscriptionHtml, SubscriptionSubstitutionTag, @@ -185,7 +186,10 @@ mail_settings = MailSettings() mail_settings.bcc_settings = BccSettings( False, BccSettingsEmail("bcc@twilio.com")) +mail_settings.bypass_bounce_management = BypassBounceManagement(False) mail_settings.bypass_list_management = BypassListManagement(False) +mail_settings.bypass_spam_management = BypassSpamManagement(False) +mail_settings.bypass_unsubscribe_management = BypassUnsubscribeManagement(False) mail_settings.footer_settings = FooterSettings( True, FooterText("w00t"), From cf0924c35c37bbec8e5ca39e963a55f54f0eec11 Mon Sep 17 00:00:00 2001 From: Huong Minh Luu Date: Tue, 20 Apr 2021 07:39:15 +0930 Subject: [PATCH 036/149] docs: Update to_emails type (#986) --- sendgrid/helpers/mail/mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 0069a3f7d..ba21f7891 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -216,8 +216,8 @@ def to(self): def to(self, to_emails, global_substitutions=None, is_multiple=False, p=0): """Adds To objects to the Personalization object - :param to_emails: An To or list of To objects - :type to_emails: To, list(To), str, tuple + :param to_emails: The email addresses of all recipients + :type to_emails: To, str, tuple, list(str), list(tuple), list(To) :param global_substitutions: A dict of substitutions for all recipients :type global_substitutions: dict :param is_multiple: Create a new personalization for each recipient From f7adbc1e83c08fbd4411fa2978b326e51549551b Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 21 Apr 2021 18:45:57 +0000 Subject: [PATCH 037/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9344367e..648cecfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-04-21] Version 6.7.0 +-------------------------- +**Library - Docs** +- [PR #986](https://github.com/sendgrid/sendgrid-python/pull/986): Update to_emails type. Thanks to [@PyGeek03](https://github.com/PyGeek03)! + +**Library - Feature** +- [PR #983](https://github.com/sendgrid/sendgrid-python/pull/983): add v3 bypass filters. Thanks to [@anarayanan604](https://github.com/anarayanan604)! + + [2021-02-10] Version 6.6.0 -------------------------- **Library - Docs** From aa39f715a061f0de993811faea0adb8223657d01 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 21 Apr 2021 18:53:46 +0000 Subject: [PATCH 038/149] Release 6.7.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index ce16fc2fb..bc09034be 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.6.0' +__version__ = '6.7.0' From 9c1b88e2f11ee2950d5be98df598c28908672a99 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Fri, 23 Apr 2021 11:10:11 -0700 Subject: [PATCH 039/149] chore: rotate key --- .travis.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c158b2b3..ea9a8fe0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,37 +1,37 @@ -dist: xenial # required for Python >= 3.7 +dist: xenial language: python cache: pip services: - - docker +- docker env: matrix: - - version=2.7 - - version=3.5 - - version=3.6 - - version=3.7 - - version=3.8 + - version=2.7 + - version=3.5 + - version=3.6 + - version=3.7 + - version=3.8 global: - - CC_TEST_REPORTER_ID=$TRAVIS_CODE_CLIMATE_TOKEN + - CC_TEST_REPORTER_ID=$TRAVIS_CODE_CLIMATE_TOKEN before_script: - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - - ./cc-test-reporter before-build +- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 + > ./cc-test-reporter +- chmod +x ./cc-test-reporter +- "./cc-test-reporter before-build" script: - - make test-docker +- make test-docker after_script: - - make test-install - - . venv/bin/activate; codecov - - ./cc-test-reporter after-build --exit-code $? +- make test-install +- ". venv/bin/activate; codecov" +- "./cc-test-reporter after-build --exit-code $?" deploy: provider: pypi - user: "__token__" - password: $PYPI_TOKEN + user: __token__ + password: "$PYPI_TOKEN" skip_cleanup: true distributions: sdist bdist_wheel on: tags: true - condition: $version = 3.6 - + condition: "$version = 3.6" notifications: slack: if: branch = main @@ -39,4 +39,4 @@ notifications: on_success: never on_failure: change rooms: - - secure: Yp7gJ6NPRPNgO77vwS0HynSdnU5LYlLlUNBEzcx+zy230UxuLLWcYZtIqsIqt4oZm45OwgJLBwoCMgmU2Jcj79rGyqWKYtUcLMLKgHVzSgxjm2outt2fxjXIJHIU60S3RCGofBJRkPwEMb7ibgwHYBEsH3wIeLrVVbWvimxka6A= + secure: damw3UZJAjoUy2Wsf9/DWT5XHIJ4DcRucS/sLPVEyynSRqhzJlxGL7gLQ2fdtMNDY+1fs4UhzYzpUIdu+Tz2mSdZlv1kRY5zIWwJ5JK+9PACt5wVXpKN794JGKcDXOh64Bqrd3ofXkyecI2OyTVNdcTu370K/Tlz3xhHvdBqpU0= From 2ac1dcbb86fb2e46ffba70a6038637ff4291d3ed Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Mon, 14 Jun 2021 18:08:14 -0700 Subject: [PATCH 040/149] Update Slack token --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ea9a8fe0a..bb2a49911 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,4 +39,4 @@ notifications: on_success: never on_failure: change rooms: - secure: damw3UZJAjoUy2Wsf9/DWT5XHIJ4DcRucS/sLPVEyynSRqhzJlxGL7gLQ2fdtMNDY+1fs4UhzYzpUIdu+Tz2mSdZlv1kRY5zIWwJ5JK+9PACt5wVXpKN794JGKcDXOh64Bqrd3ofXkyecI2OyTVNdcTu370K/Tlz3xhHvdBqpU0= + secure: n7ZtDd7AvPsw7Wd6fOCCWYiakWpCnYs1QqXpiozF9Rh1Z90XcrQp72utPFyl81jJp7zlgYWXSfHqmnUpbOCWd04WTsC4dkAY6dd/ThARV4kRkxONX3nlbRESOeZIWNXNOeSR1pg6sd9H7xwIDGmmN2arnFRNiQAD0y5li0yxAfQ= From 23e696bd021d44ccc158b62a0edbbf3214bc426b Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Mon, 14 Jun 2021 19:35:02 -0700 Subject: [PATCH 041/149] chore: remove logic adding quotes to names containing , and ; (#994) --- sendgrid/helpers/mail/email.py | 19 ------------------- test/test_email.py | 14 -------------- 2 files changed, 33 deletions(-) diff --git a/sendgrid/helpers/mail/email.py b/sendgrid/helpers/mail/email.py index ba3a98848..aeab26afa 100644 --- a/sendgrid/helpers/mail/email.py +++ b/sendgrid/helpers/mail/email.py @@ -3,20 +3,6 @@ except ImportError: import email.utils as rfc822 -import sys -if sys.version_info[:3] >= (3, 5, 0): - import html - html_entity_decode = html.unescape -else: - try: - # Python 2.6-2.7 - from HTMLParser import HTMLParser - except ImportError: - # Python < 3.5 - from html.parser import HTMLParser - __html_parser__ = HTMLParser() - html_entity_decode = __html_parser__.unescape - try: basestring = basestring except NameError: @@ -91,11 +77,6 @@ def name(self, value): if not (value is None or isinstance(value, basestring)): raise TypeError('name must be of type string.') - # Escape common CSV delimiters as workaround for - # https://github.com/sendgrid/sendgrid-python/issues/578 - if value is not None and (',' in value or ';' in value): - value = html_entity_decode(value) - value = '"' + value + '"' self._name = value @property diff --git a/test/test_email.py b/test/test_email.py index eb50374aa..9db060705 100644 --- a/test/test_email.py +++ b/test/test_email.py @@ -66,17 +66,3 @@ def test_empty_obj_add_email(self): email.email = address self.assertEqual(email.email, address) - - def test_add_name_with_comma(self): - email = Email() - name = "Name, Some" - email.name = name - - self.assertEqual(email.name, '"' + name + '"') - - def test_add_unicode_name_with_comma(self): - email = Email() - name = u"Name, Some" - email.name = name - - self.assertEqual(email.name, u'"' + name + u'"') From be104f1ff1c2b6220cfa1b3ef7375c1b4d0fa118 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Mon, 14 Jun 2021 22:29:00 -0700 Subject: [PATCH 042/149] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bb2a49911..a4c7241db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,4 +39,4 @@ notifications: on_success: never on_failure: change rooms: - secure: n7ZtDd7AvPsw7Wd6fOCCWYiakWpCnYs1QqXpiozF9Rh1Z90XcrQp72utPFyl81jJp7zlgYWXSfHqmnUpbOCWd04WTsC4dkAY6dd/ThARV4kRkxONX3nlbRESOeZIWNXNOeSR1pg6sd9H7xwIDGmmN2arnFRNiQAD0y5li0yxAfQ= + secure: p48e21acIxzCs/vFcrSGvcv3jk/PnLcI8BaFpek7KE6lFUi+lDOlg3tNJlqBwQh2BKuoFdd4+x5uNcfpm3cl32usPA2K4e5EX6su+yJcrtt18kAt+p9AjrqFqxgKiq3gKCF/Bh8+r+yk8wMgS+WU8Bg2z6cwUqAoy5OcFwkvYu0wbDkFtEwWljXZbWejfEGD5OEq/4aZzM0GNl3DRdVcU7l4p0A3xPLIUJDSjKQ4J3GZSZE64YqHH1ANJergcX6mmMGVIQEHzgAXXBcLanzxTQfySgrrVMJz/xZh4lRJ/EMxMDj9LXFjOgQxJfo5qgPfhgc+s1hFajS0ykcJZW0Y7DnJz42Bjw4HnQayxEoB4/2HBD2Osggkd6mshe86QNzi1Xjd/V+Bs/RfuFHiU63AuAn0F1VHuOyrFu55MDCaJTg5RoWigP3k8cIlIMPAYygdxwB++FwcMiEdnoV9Coh4Lx6d6UNctGUOM22Dlnchn0CXbIb6/jqJA0atM9RvP3O0tcgD1xcN6IfiF55QAkd3E3K1aC+9pvy8UnD6biLf3k6YvaVrO/9ds+KbAhvQhnTnmhc++yAOdb24pbFQQVUYbn6/9nUkFs0Qw5yNP4Smp8dLOL/m9iwSwIqfclVY8GgYzjdDvib7NwWuB2IaHvbNPCsNx7PRtT81RNASoxc/+aM= From 33598f74b64c8da91867bdf5f2b9bf1a74057de8 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 16 Jun 2021 20:55:24 +0000 Subject: [PATCH 043/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 648cecfeb..90674eee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-06-16] Version 6.7.1 +-------------------------- +**Library - Chore** +- [PR #994](https://github.com/sendgrid/sendgrid-python/pull/994): remove logic adding quotes to names containing , and ;. Thanks to [@JenniferMah](https://github.com/JenniferMah)! + + [2021-04-21] Version 6.7.0 -------------------------- **Library - Docs** From 8d9d1f0df0072755a8bb4a71b67c70a3bc6fd072 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 16 Jun 2021 21:05:58 +0000 Subject: [PATCH 044/149] Release 6.7.1 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index bc09034be..157048dac 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.7.0' +__version__ = '6.7.1' From 44dbf5c27da9f47291b5a1fa656b711cce418f53 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 16 Jun 2021 16:27:21 -0700 Subject: [PATCH 045/149] update slack token --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a4c7241db..35f9fdc2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,4 +39,4 @@ notifications: on_success: never on_failure: change rooms: - secure: p48e21acIxzCs/vFcrSGvcv3jk/PnLcI8BaFpek7KE6lFUi+lDOlg3tNJlqBwQh2BKuoFdd4+x5uNcfpm3cl32usPA2K4e5EX6su+yJcrtt18kAt+p9AjrqFqxgKiq3gKCF/Bh8+r+yk8wMgS+WU8Bg2z6cwUqAoy5OcFwkvYu0wbDkFtEwWljXZbWejfEGD5OEq/4aZzM0GNl3DRdVcU7l4p0A3xPLIUJDSjKQ4J3GZSZE64YqHH1ANJergcX6mmMGVIQEHzgAXXBcLanzxTQfySgrrVMJz/xZh4lRJ/EMxMDj9LXFjOgQxJfo5qgPfhgc+s1hFajS0ykcJZW0Y7DnJz42Bjw4HnQayxEoB4/2HBD2Osggkd6mshe86QNzi1Xjd/V+Bs/RfuFHiU63AuAn0F1VHuOyrFu55MDCaJTg5RoWigP3k8cIlIMPAYygdxwB++FwcMiEdnoV9Coh4Lx6d6UNctGUOM22Dlnchn0CXbIb6/jqJA0atM9RvP3O0tcgD1xcN6IfiF55QAkd3E3K1aC+9pvy8UnD6biLf3k6YvaVrO/9ds+KbAhvQhnTnmhc++yAOdb24pbFQQVUYbn6/9nUkFs0Qw5yNP4Smp8dLOL/m9iwSwIqfclVY8GgYzjdDvib7NwWuB2IaHvbNPCsNx7PRtT81RNASoxc/+aM= + secure: CTG+XvHwzZlUuc8Gdq96+sK9w1FbkRCBg0yiDGtXFVK8fWFPQEwxLBWQ+yL7vzkS1RMc+Ib7ML4tKU2CMlVxgATOT7RetOioFw56RyV/fd+9eon1PONuz+d5HTIHm64GMhKSxvC4TQbUA4m4+kRX8bCsCfUpTz90HpTm5nSXZ/o= From 5eb3aaef6e96cd0469f3f1ffc334a42e67c3bf8e Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 16 Jun 2021 16:39:31 -0700 Subject: [PATCH 046/149] chore: add docker credentials to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a4c7241db..a9a470a26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ before_script: - chmod +x ./cc-test-reporter - "./cc-test-reporter before-build" script: +- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - make test-docker after_script: - make test-install From 5a31c75568a574b984e0f8aa76a5e8700d23ab0e Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 16 Jun 2021 16:44:30 -0700 Subject: [PATCH 047/149] update slack token --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3d55a26d7..02c163356 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,4 +40,4 @@ notifications: on_success: never on_failure: change rooms: - secure: CTG+XvHwzZlUuc8Gdq96+sK9w1FbkRCBg0yiDGtXFVK8fWFPQEwxLBWQ+yL7vzkS1RMc+Ib7ML4tKU2CMlVxgATOT7RetOioFw56RyV/fd+9eon1PONuz+d5HTIHm64GMhKSxvC4TQbUA4m4+kRX8bCsCfUpTz90HpTm5nSXZ/o= + secure: B0SJHc9Syyf5HOl21abg/Uj/Gp7EusCOly/2JZzUUHCWtxC8C9pWfGf2e674R4vdeJ3FmTKz/1jJZ96vzV0z+XUpT2Fnn6URi4kjI8C0XNTs8la+bz5riSM4TokYOv0HGbL/r0OmHraodCxuX1rpkcYX+FD1dwcGC70eEB6NIu4= From c905e05a80fea6cd06189522d3f760cffd6c769d Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 21 Jul 2021 15:27:34 -0700 Subject: [PATCH 048/149] Remove newsletter badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index de23ef2f6..8b089aeb3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Travis Badge](https://travis-ci.com/sendgrid/sendgrid-python.svg?branch=main)](https://travis-ci.com/sendgrid/sendgrid-python) [![codecov](https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/main.svg?style=flat-square&label=Codecov+Coverage)](https://codecov.io/gh/sendgrid/sendgrid-python) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) -[![Email Notifications Badge](https://dx.sendgrid.com/badge/python)](https://dx.sendgrid.com/newsletter/python) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) [![GitHub contributors](https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg)](https://github.com/sendgrid/sendgrid-python/graphs/contributors) From 25c3de49aea06a02b66ab2645583f52bc3c2881a Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Thu, 29 Jul 2021 14:29:36 -0700 Subject: [PATCH 049/149] chore: remove docker credentials for PRs --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 02c163356..2341f8631 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,9 @@ before_script: - chmod +x ./cc-test-reporter - "./cc-test-reporter before-build" script: -- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin +- if [[ "$TRAVIS_BRANCH" == "main" || "$TRAVIS_BRANCH" == "travis" ]] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then + echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin + fi - make test-docker after_script: - make test-install From c99e8ea481bdc2c036f8fe9971c732ef3cb598a2 Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Thu, 29 Jul 2021 14:33:09 -0700 Subject: [PATCH 050/149] chore: revert removal of docker credentials for PRs --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2341f8631..02c163356 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,7 @@ before_script: - chmod +x ./cc-test-reporter - "./cc-test-reporter before-build" script: -- if [[ "$TRAVIS_BRANCH" == "main" || "$TRAVIS_BRANCH" == "travis" ]] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin - fi +- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - make test-docker after_script: - make test-install From 4047acd5e131b3942308f7c09fb5577b9bf6aa15 Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Thu, 29 Jul 2021 14:46:56 -0700 Subject: [PATCH 051/149] chore: remove docker credentials for PRs --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 02c163356..76606df83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,9 @@ before_script: - chmod +x ./cc-test-reporter - "./cc-test-reporter before-build" script: -- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin +- if [[ "$TRAVIS_BRANCH" == "main" || "$TRAVIS_BRANCH" == "travis" ]] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then + echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin; + fi - make test-docker after_script: - make test-install From 272274ae26943bf0d7433843dab914ab9fcb4993 Mon Sep 17 00:00:00 2001 From: vindarel Date: Fri, 30 Jul 2021 17:11:45 +0200 Subject: [PATCH 052/149] feat: add reply_to to helpers.Mail (#999) * Add reply_to to helpers.Mail Otherwise, we must create the Mail and then set the reply_to thanks to its setter. (and we have to dig the code to find out). * fix: tests: add missing import Co-authored-by: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> --- sendgrid/helpers/mail/mail.py | 7 +++++++ test/test_mail_helpers.py | 7 ++++++- use_cases/send_a_single_email_to_a_single_recipient.md | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index ba21f7891..ba04a2b5b 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -24,6 +24,7 @@ def __init__( self, from_email=None, to_emails=None, + reply_to=None, subject=None, plain_text_content=None, html_content=None, @@ -40,6 +41,8 @@ def __init__( :param to_emails: The email address of the recipient :type to_emails: To, str, tuple, list(str), list(tuple), list(To), optional + :param reply_to: The email address to reply to + :type reply_to: ReplyTo, tuple, optional :param plain_text_content: The plain text body of the email :type plain_text_content: string, optional :param html_content: The html body of the email @@ -79,6 +82,10 @@ def __init__( if html_content is not None: self.add_content(html_content, MimeType.html) + # Optional + if reply_to is not None: + self.reply_to = reply_to + def __str__(self): """A JSON-ready string representation of this Mail object. diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index 57a4ba880..49c58d05b 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -94,12 +94,13 @@ def test_batch_id(self): # Send a Single Email to a Single Recipient def test_single_email_to_a_single_recipient(self): - from sendgrid.helpers.mail import (Mail, From, To, Subject, + from sendgrid.helpers.mail import (Mail, From, To, ReplyTo, Subject, PlainTextContent, HtmlContent) self.maxDiff = None message = Mail( from_email=From('test+from@example.com', 'Example From Name'), to_emails=To('test+to@example.com', 'Example To Name'), + reply_to=ReplyTo('test+reply_to@example.com', 'Example Reply To Name'), subject=Subject('Sending with SendGrid is Fun'), plain_text_content=PlainTextContent( 'and easy to do anywhere, even with Python'), @@ -123,6 +124,10 @@ def test_single_email_to_a_single_recipient(self): "email": "test+from@example.com", "name": "Example From Name" }, + "reply_to": { + "email": "test+reply_to@example.com", + "name": "Example Reply To Name" + }, "personalizations": [ { "to": [ diff --git a/use_cases/send_a_single_email_to_a_single_recipient.md b/use_cases/send_a_single_email_to_a_single_recipient.md index 8a2364285..c469c3bd6 100644 --- a/use_cases/send_a_single_email_to_a_single_recipient.md +++ b/use_cases/send_a_single_email_to_a_single_recipient.md @@ -6,6 +6,7 @@ from sendgrid.helpers.mail import Mail message = Mail( from_email='from_email@example.com', to_emails='to@example.com', + reply_to='reply_to@example.com', subject='Sending with Twilio SendGrid is Fun', html_content='and easy to do anywhere, even with Python') try: From c0da90440f25c82648b0fda0d9e62f02ec8a55e8 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 11 Aug 2021 16:45:38 +0000 Subject: [PATCH 053/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90674eee0..ad87a42d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-08-11] Version 6.8.0 +-------------------------- +**Library - Feature** +- [PR #999](https://github.com/sendgrid/sendgrid-python/pull/999): add reply_to to helpers.Mail. Thanks to [@vindarel](https://github.com/vindarel)! + + [2021-06-16] Version 6.7.1 -------------------------- **Library - Chore** From 08f0670aa2a5d05cfe981e3584dcd491d469b26d Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 11 Aug 2021 16:52:36 +0000 Subject: [PATCH 054/149] Release 6.8.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 157048dac..ff52e2e6c 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.7.1' +__version__ = '6.8.0' From 62d43ed0b27c8f7eec6dad8fe3880b81be7b1d2c Mon Sep 17 00:00:00 2001 From: Shwetha Radhakrishna Date: Wed, 18 Aug 2021 12:57:37 -0500 Subject: [PATCH 055/149] chore: revert reply_to prop add in mail (#1003) Co-authored-by: Shwetha Radhakrishna --- sendgrid/helpers/mail/mail.py | 7 ------- test/test_mail_helpers.py | 9 ++------- use_cases/send_a_single_email_to_a_single_recipient.md | 1 - 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index ba04a2b5b..ba21f7891 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -24,7 +24,6 @@ def __init__( self, from_email=None, to_emails=None, - reply_to=None, subject=None, plain_text_content=None, html_content=None, @@ -41,8 +40,6 @@ def __init__( :param to_emails: The email address of the recipient :type to_emails: To, str, tuple, list(str), list(tuple), list(To), optional - :param reply_to: The email address to reply to - :type reply_to: ReplyTo, tuple, optional :param plain_text_content: The plain text body of the email :type plain_text_content: string, optional :param html_content: The html body of the email @@ -82,10 +79,6 @@ def __init__( if html_content is not None: self.add_content(html_content, MimeType.html) - # Optional - if reply_to is not None: - self.reply_to = reply_to - def __str__(self): """A JSON-ready string representation of this Mail object. diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index 49c58d05b..1598c607b 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -94,13 +94,12 @@ def test_batch_id(self): # Send a Single Email to a Single Recipient def test_single_email_to_a_single_recipient(self): - from sendgrid.helpers.mail import (Mail, From, To, ReplyTo, Subject, + from sendgrid.helpers.mail import (Mail, From, To, Subject, PlainTextContent, HtmlContent) self.maxDiff = None message = Mail( from_email=From('test+from@example.com', 'Example From Name'), to_emails=To('test+to@example.com', 'Example To Name'), - reply_to=ReplyTo('test+reply_to@example.com', 'Example Reply To Name'), subject=Subject('Sending with SendGrid is Fun'), plain_text_content=PlainTextContent( 'and easy to do anywhere, even with Python'), @@ -124,10 +123,6 @@ def test_single_email_to_a_single_recipient(self): "email": "test+from@example.com", "name": "Example From Name" }, - "reply_to": { - "email": "test+reply_to@example.com", - "name": "Example Reply To Name" - }, "personalizations": [ { "to": [ @@ -660,7 +655,7 @@ def test_personalization_add_email_filters_out_duplicate_to_emails(self): p.add_email(to_email) self.assertEqual([to_email.get()], p.tos) - + def test_personalization_add_email_filters_out_duplicate_to_emails_ignoring_case(self): self.maxDiff = None diff --git a/use_cases/send_a_single_email_to_a_single_recipient.md b/use_cases/send_a_single_email_to_a_single_recipient.md index c469c3bd6..8a2364285 100644 --- a/use_cases/send_a_single_email_to_a_single_recipient.md +++ b/use_cases/send_a_single_email_to_a_single_recipient.md @@ -6,7 +6,6 @@ from sendgrid.helpers.mail import Mail message = Mail( from_email='from_email@example.com', to_emails='to@example.com', - reply_to='reply_to@example.com', subject='Sending with Twilio SendGrid is Fun', html_content='and easy to do anywhere, even with Python') try: From 395f86d2feb04a00d510b4107e543edaf0506d80 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 25 Aug 2021 18:43:36 +0000 Subject: [PATCH 056/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad87a42d9..7ae148d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-08-25] Version 6.8.1 +-------------------------- +**Library - Chore** +- [PR #1003](https://github.com/sendgrid/sendgrid-python/pull/1003): get rid of reply_to in mail helper. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + [2021-08-11] Version 6.8.0 -------------------------- **Library - Feature** From e40816321149fa360de297eab49979c3e522c825 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 25 Aug 2021 18:48:28 +0000 Subject: [PATCH 057/149] Release 6.8.1 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index ff52e2e6c..c5a38a4f5 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.8.0' +__version__ = '6.8.1' From a9a80675937ff3b9d8d9719e95a0b87cb42929ae Mon Sep 17 00:00:00 2001 From: Shwetha Radhakrishna Date: Fri, 10 Sep 2021 12:05:00 -0500 Subject: [PATCH 058/149] chore: add tests for v3.9 (#1007) --- .travis.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 76606df83..46f3601b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ env: - version=3.6 - version=3.7 - version=3.8 + - version=3.9 global: - CC_TEST_REPORTER_ID=$TRAVIS_CODE_CLIMATE_TOKEN before_script: diff --git a/README.md b/README.md index 8b089aeb3..8ce206abb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ We appreciate your continued support, thank you! ## Prerequisites -- Python version 2.7, 3.5, 3.6, 3.7, or 3.8 +- Python version 2.7+ - The SendGrid service, starting at the [free level](https://sendgrid.com/free?source=sendgrid-python) ## Setup Environment Variables From 14a07cd85a51d4c26d9fe828bb829dca5e2ad401 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 22 Sep 2021 21:05:13 +0000 Subject: [PATCH 059/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae148d6d..f4712dc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-09-22] Version 6.8.2 +-------------------------- +**Library - Chore** +- [PR #1007](https://github.com/sendgrid/sendgrid-python/pull/1007): test against v3.9. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + [2021-08-25] Version 6.8.1 -------------------------- **Library - Chore** From c22b7f6ca97a750ac68042d706ffd34af6fde429 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 22 Sep 2021 21:13:09 +0000 Subject: [PATCH 060/149] Release 6.8.2 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index c5a38a4f5..94859745c 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.8.1' +__version__ = '6.8.2' From 0d00f0dfe98a8c442c66f891f6fca18234418606 Mon Sep 17 00:00:00 2001 From: Shwetha Radhakrishna Date: Fri, 8 Oct 2021 06:51:53 -0700 Subject: [PATCH 061/149] docs: improve signed event webhook validation docs (#1013) --- TROUBLESHOOTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index b9a68b8c0..0a7f54c69 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -15,6 +15,7 @@ If you can't find a solution below, please open an [issue](https://github.com/se * [Version Convention](#versions) * [Viewing the Request Body](#request-body) * [Error Handling](#error-handling) +* [Verifying Event Webhooks](#signed-webhooks) ## Environment Variables and Your Twilio SendGrid API Key @@ -117,3 +118,14 @@ You can do this right before you call `response = sg.client.mail.send.post(reque # Error Handling Please review [our use_cases](use_cases/README.md) for examples of error handling. + + +## Signed Webhook Verification + +Twilio SendGrid's Event Webhook will notify a URL via HTTP POST with information about events that occur as your mail is processed. [This](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features) article covers all you need to know to secure the Event Webhook, allowing you to verify that incoming requests originate from Twilio SendGrid. The sendgrid-python library can help you verify these Signed Event Webhooks. + +You can find the usage example [here](examples/helpers/eventwebhook/eventwebhook_example.py) and the tests [here](test/test_eventwebhook.py). +If you are still having trouble getting the validation to work, follow the following instructions: +- Be sure to use the *raw* payload for validation +- Be sure to include a trailing carriage return and newline in your payload +- In case of multi-event webhooks, make sure you include the trailing newline and carriage return after *each* event From 77ae5f5c7ae69a49d9814d96ccc418a31c3271cd Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Mon, 11 Oct 2021 11:31:14 -0700 Subject: [PATCH 062/149] chore: pin starkbank-ecdsa version (#1015) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ff1ba3c35..37e68e9d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ PyYAML>=4.2b1 python-http-client>=3.2.1 six==1.11.0 pytest==3.8.2 -starkbank-ecdsa>=1.0.0 +starkbank-ecdsa>=1.0.0,<2.0.0 From 01b72926140af023bfa2546d09dbb3b9be126a90 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Mon, 11 Oct 2021 13:41:24 -0700 Subject: [PATCH 063/149] chore: pin starkbank-ecdsa version (#1016) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3eeec82d..8ec7329c8 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'starkbank-ecdsa>=1.0.0' + 'starkbank-ecdsa>=1.0.0,<2.0.0' ] return deps From 3fdf07930123f494088ab276441d538ac11ef9fa Mon Sep 17 00:00:00 2001 From: Twilio Date: Mon, 18 Oct 2021 18:31:15 +0000 Subject: [PATCH 064/149] [Librarian] Version Bump --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4712dc13..53d21c256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-10-18] Version 6.8.3 +-------------------------- +**Library - Chore** +- [PR #1016](https://github.com/sendgrid/sendgrid-python/pull/1016): pin starkbank-ecdsa version. Thanks to [@eshanholtz](https://github.com/eshanholtz)! +- [PR #1015](https://github.com/sendgrid/sendgrid-python/pull/1015): pin starkbank-ecdsa version. Thanks to [@eshanholtz](https://github.com/eshanholtz)! + +**Library - Docs** +- [PR #1013](https://github.com/sendgrid/sendgrid-python/pull/1013): improve signed event webhook validation docs. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! + + [2021-09-22] Version 6.8.2 -------------------------- **Library - Chore** From 2e12f1cc86f7fc59d1fd0cea0030d0213aee493f Mon Sep 17 00:00:00 2001 From: Twilio Date: Mon, 18 Oct 2021 18:35:05 +0000 Subject: [PATCH 065/149] Release 6.8.3 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 94859745c..b45da21b6 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.8.2' +__version__ = '6.8.3' From a97e83a15dffe41cd87f52316fe04502314aec39 Mon Sep 17 00:00:00 2001 From: Bilal Boussayoud Date: Mon, 25 Oct 2021 15:07:33 -0600 Subject: [PATCH 066/149] feat: allow personalization of the From name and email for each recipient (#1020) * feat: allow personalization of the From name and email for each recipient --- examples/helpers/README.md | 12 ++-- examples/helpers/mail_example.py | 65 ++++++++++++++++--- sendgrid/helpers/mail/personalization.py | 21 +++++- test/test_mail_helpers.py | 9 +++ use_cases/README.md | 1 + .../send_multiple_emails_personalizations.md | 32 +++++++++ 6 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 use_cases/send_multiple_emails_personalizations.md diff --git a/examples/helpers/README.md b/examples/helpers/README.md index df1746b52..8d7594d44 100644 --- a/examples/helpers/README.md +++ b/examples/helpers/README.md @@ -28,12 +28,16 @@ For more information on parameters and usage, see [here](../mail/mail.py) ### Creating Personalizations -To create personalizations, you need a dictionary to store all your email components. See example [here](https://github.com/sendgrid/sendgrid-python/blob/0b683169b08d3a7c204107cd333be33053297e74/examples/helpers/mail_example.py#L47) -After creating a dictionary, you can go ahead and create a `Personalization` object. +The personalization helper can be used to create personalizations and customize various aspects of an email. See example [here](mail_example.py) in `build_multiple_emails_personalized()`, and refer [here](https://docs.sendgrid.com/for-developers/sending-email/personalizations) for more documentation. ``` mock_personalization = Personalization() - for to_addr in personalization['to_list']: - mock_personalization.add_to(to_addr) + + for to_addr in personalization['to_list']: + mock_personalization.add_to(to_addr) + + mock_personalization.set_from(from_addr) + mock_personalization.add_cc(cc_addr) + # etc... ``` ### Creating Attachments diff --git a/examples/helpers/mail_example.py b/examples/helpers/mail_example.py index 700970110..384181501 100644 --- a/examples/helpers/mail_example.py +++ b/examples/helpers/mail_example.py @@ -13,7 +13,7 @@ def build_hello_email(): from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException - message = Mail(from_email=From('from@example.com.com', 'Example From Name'), + message = Mail(from_email=From('from@example.com', 'Example From Name'), to_emails=To('to@example.com', 'Example To Name'), subject=Subject('Sending with SendGrid is Fun'), plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), @@ -26,25 +26,30 @@ def build_hello_email(): except SendGridException as e: print(e.message) - for cc_addr in personalization['cc_list']: + mock_personalization = Personalization() + personalization_dict = get_mock_personalization_dict() + + for cc_addr in personalization_dict['cc_list']: mock_personalization.add_to(cc_addr) - for bcc_addr in personalization['bcc_list']: + for bcc_addr in personalization_dict['bcc_list']: mock_personalization.add_bcc(bcc_addr) - for header in personalization['headers']: + for header in personalization_dict['headers']: mock_personalization.add_header(header) - for substitution in personalization['substitutions']: + for substitution in personalization_dict['substitutions']: mock_personalization.add_substitution(substitution) - for arg in personalization['custom_args']: + for arg in personalization_dict['custom_args']: mock_personalization.add_custom_arg(arg) - mock_personalization.subject = personalization['subject'] - mock_personalization.send_at = personalization['send_at'] - return mock_personalization + mock_personalization.subject = personalization_dict['subject'] + mock_personalization.send_at = personalization_dict['send_at'] + + message.add_personalization(mock_personalization) + return message def get_mock_personalization_dict(): """Get a dict of personalization mock.""" @@ -78,6 +83,36 @@ def get_mock_personalization_dict(): mock_pers['send_at'] = 1443636843 return mock_pers +def build_multiple_emails_personalized(): + import json + from sendgrid.helpers.mail import Mail, From, To, Cc, Bcc, Subject, PlainTextContent, \ + HtmlContent, SendGridException, Personalization + + # Note that the domain for all From email addresses must match + message = Mail(from_email=From('from@example.com', 'Example From 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')) + + mock_personalization = Personalization() + mock_personalization.add_to(To('test@example.com', 'Example User 1')) + mock_personalization.add_cc(Cc('test1@example.com', 'Example User 2')) + message.add_personalization(mock_personalization) + + mock_personalization_2 = Personalization() + mock_personalization_2.add_to(To('test2@example.com', 'Example User 3')) + mock_personalization_2.set_from(From('from@example.com', 'Example From Name 2')) + mock_personalization_2.add_bcc(Bcc('test3@example.com', 'Example User 4')) + message.add_personalization(mock_personalization_2) + + try: + print(json.dumps(message.get(), sort_keys=True, indent=4)) + return message.get() + + except SendGridException as e: + print(e.message) + + return message def build_attachment1(): """Build attachment mock. Make sure your content is base64 encoded before passing into attachment.content. @@ -308,6 +343,15 @@ def build_kitchen_sink(): return message +def send_multiple_emails_personalized(): + # Assumes you set your environment variable: + # https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key + message = build_multiple_emails_personalized() + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message=message) + print(response.status_code) + print(response.body) + print(response.headers) def send_hello_email(): # Assumes you set your environment variable: @@ -334,5 +378,8 @@ def send_kitchen_sink(): ## this will actually send an email # send_hello_email() +## this will send multiple emails +# send_multiple_emails_personalized() + ## this will only send an email if you set SandBox Mode to False # send_kitchen_sink() diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 21a31c863..a4e1c1de4 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -6,6 +6,7 @@ class Personalization(object): def __init__(self): """Create an empty Personalization and initialize member variables.""" self._tos = [] + self._from_email = None self._ccs = [] self._bccs = [] self._subject = None @@ -26,7 +27,10 @@ def add_email(self, email): if email_type.__name__ == 'Bcc': self.add_bcc(email) return - raise ValueError('Please use a To, Cc or Bcc object.') + if email_type.__name__ == 'From': + self.from_email = email + return + raise ValueError('Please use a To, From, Cc or Bcc object.') def _get_unique_recipients(self, recipients): unique_recipients = [] @@ -77,6 +81,17 @@ def add_to(self, email): self._tos.append(email.get()) + @property + def from_email(self): + return self._from_email + + @from_email.setter + def from_email(self, value): + self._from_email = value + + def set_from(self, email): + self._from_email = email.get() + @property def ccs(self): """A list of recipients who will receive copies of this email. @@ -236,6 +251,10 @@ def get(self): if value: personalization[key[:-1]] = value + from_value = getattr(self, 'from_email') + if from_value: + personalization['from'] = from_value + for key in ['subject', 'send_at', 'dynamic_template_data']: value = getattr(self, key) if value: diff --git a/test/test_mail_helpers.py b/test/test_mail_helpers.py index 1598c607b..a7d08d890 100644 --- a/test/test_mail_helpers.py +++ b/test/test_mail_helpers.py @@ -667,6 +667,15 @@ def test_personalization_add_email_filters_out_duplicate_to_emails_ignoring_case self.assertEqual([to_email.get()], p.tos) + def test_personalization_set_from_email(self): + self.maxDiff = None + + p = Personalization() + from_email = From('test+from@example.com', 'Example From') + p.set_from(from_email) + + self.assertEqual(from_email.get(), p.from_email) + def test_personalization_filters_out_duplicate_cc_emails(self): self.maxDiff = None diff --git a/use_cases/README.md b/use_cases/README.md index a91f1f5a4..f9fe2470e 100644 --- a/use_cases/README.md +++ b/use_cases/README.md @@ -8,6 +8,7 @@ This directory provides examples for specific use cases of this library. Please * [Send a Single Email to a Single Recipient](send_a_single_email_to_a_single_recipient.md) * [Send a Single Email to Multiple Recipients](send_a_single_email_to_multiple_recipients.md) * [Send Multiple Emails to Multiple Recipients](send_multiple_emails_to_multiple_recipients.md) +* [Send Multiple Emails with Personalizations](send_multiple_emails_personalizations.md) * [Kitchen Sink - an example with all settings used](kitchen_sink.md) * [Transactional Templates](transactional_templates.md) * [Attachments](attachment.md) diff --git a/use_cases/send_multiple_emails_personalizations.md b/use_cases/send_multiple_emails_personalizations.md new file mode 100644 index 000000000..e8a7e2eec --- /dev/null +++ b/use_cases/send_multiple_emails_personalizations.md @@ -0,0 +1,32 @@ +```python +import os +import json +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, Personalization, From, To, Cc, Bcc + +# Note that the domain for all From email addresses must match +message = Mail( + from_email=('from@example.com', 'Example From Name'), + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python') + +personalization1 = Personalization() +personalization1.add_email(To('test0@example.com', 'Example Name 0')) +personalization1.add_email(Cc('test1@example.com', 'Example Name 1')) +message.add_personalization(personalization1) + +personalization2 = Personalization() +personalization2.add_email(To('test2@example.com', 'Example Name 2')) +personalization2.add_email(Bcc('test3@example.com', 'Example Name 3')) +personalization2.add_email(From('from2@example.com', 'Example From Name 2')) +message.add_personalization(personalization2) + +try: + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e.message) +``` \ No newline at end of file From 5e9b0ff061a3cb888f7089473b7039432b748f37 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 3 Nov 2021 18:51:55 +0000 Subject: [PATCH 067/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d21c256..8004be4db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-11-03] Version 6.9.0 +-------------------------- +**Library - Feature** +- [PR #1020](https://github.com/sendgrid/sendgrid-python/pull/1020): allow personalization of the From name and email for each recipient. Thanks to [@beebzz](https://github.com/beebzz)! + + [2021-10-18] Version 6.8.3 -------------------------- **Library - Chore** From ab040cb25696a900224789cb2f97bbf5193fd307 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 3 Nov 2021 18:55:47 +0000 Subject: [PATCH 068/149] Release 6.9.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index b45da21b6..a51875d3a 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.8.3' +__version__ = '6.9.0' From e27a2b1b7c1102710f10fe6674e6b93c64431666 Mon Sep 17 00:00:00 2001 From: hellno Date: Tue, 9 Nov 2021 23:59:21 +0100 Subject: [PATCH 069/149] chore: fix vulnerability in starbank-ecdsa dependency (#1022) Co-authored-by: Jennifer Mah --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 37e68e9d9..04c3b87e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ PyYAML>=4.2b1 python-http-client>=3.2.1 six==1.11.0 pytest==3.8.2 -starkbank-ecdsa>=1.0.0,<2.0.0 +starkbank-ecdsa>=2.0.1 diff --git a/setup.py b/setup.py index 8ec7329c8..365ebb50b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'starkbank-ecdsa>=1.0.0,<2.0.0' + 'starkbank-ecdsa>=2.0.1' ] return deps From 05557b665dd6edfa41e1290c36dfa9fd76be2762 Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Tue, 9 Nov 2021 16:32:32 -0800 Subject: [PATCH 070/149] fix: fix event webhook for updated starbank-ecdsa dependency --- sendgrid/helpers/eventwebhook/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/helpers/eventwebhook/__init__.py b/sendgrid/helpers/eventwebhook/__init__.py index a44eb5b89..c1ec7d1c8 100644 --- a/sendgrid/helpers/eventwebhook/__init__.py +++ b/sendgrid/helpers/eventwebhook/__init__.py @@ -27,7 +27,7 @@ def convert_public_key_to_ecdsa(self, public_key): :return: public key using the ECDSA algorithm :rtype PublicKey """ - return PublicKey.fromPem(public_key) + return PublicKey.fromPem('\n-----BEGIN PUBLIC KEY-----\n'+public_key+'\n-----END PUBLIC KEY-----\n') def verify_signature(self, payload, signature, timestamp, public_key=None): """ From a3cbe32bd270db4fc2d81b92f73baf6ddab113a7 Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Tue, 9 Nov 2021 17:25:30 -0800 Subject: [PATCH 071/149] Chore: pin more-itertools version --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 04c3b87e8..b2ba4d1be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ python-http-client>=3.2.1 six==1.11.0 pytest==3.8.2 starkbank-ecdsa>=2.0.1 +more-itertools==5.0.0 From f5d6a3837d67d4ed479d4509c14822ad95730291 Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:58:16 -0800 Subject: [PATCH 072/149] docs: fix event webhook documentation --- use_cases/email_stats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/use_cases/email_stats.md b/use_cases/email_stats.md index c40ccb882..10e265721 100644 --- a/use_cases/email_stats.md +++ b/use_cases/email_stats.md @@ -2,4 +2,4 @@ You can find documentation for how to view your email statistics via the UI [here](https://app.sendgrid.com/statistics) and via API [here](../USAGE.md#stats). -Alternatively, we can post events to a URL of your choice via our [Event Webhook](https://sendgrid.com/docs/API_Reference/Webhooks/event.html) about events that occur as Twilio SendGrid processes your email. \ No newline at end of file +Alternatively, we can post events to a URL of your choice via our [Event Webhook](https://docs.sendgrid.com/for-developers/tracking-events/event) about events that occur as Twilio SendGrid processes your email. From 26009631c6490b2e38f6daf7febe3fe7182699f6 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 17 Nov 2021 19:24:12 +0000 Subject: [PATCH 073/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8004be4db..35318143e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-11-17] Version 6.9.1 +-------------------------- +**Library - Chore** +- [PR #1022](https://github.com/sendgrid/sendgrid-python/pull/1022): fix vulnerability in starbank-ecdsa dependency. Thanks to [@hellno](https://github.com/hellno)! + + [2021-11-03] Version 6.9.0 -------------------------- **Library - Feature** From 1565945d8f9025946ac8a79821ab07444db68e41 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 17 Nov 2021 19:28:16 +0000 Subject: [PATCH 074/149] Release 6.9.1 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index a51875d3a..ae7f14541 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.0' +__version__ = '6.9.1' From 744ca8cf1d7c0ae8fe29c86f18ad89718c99b830 Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Wed, 1 Dec 2021 10:36:41 -0800 Subject: [PATCH 075/149] chore: migrate to GitHub Actions (#1027) --- .codeclimate.yml | 3 --- .github/workflows/release.yml | 48 +++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 50 +++++++++++++++++++++++++++++++++++ .travis.yml | 46 -------------------------------- Makefile | 1 + README.md | 3 +-- README.rst | 6 ++--- requirements.txt | 2 +- test/test_project.py | 8 ------ test/test_sendgrid.py | 4 +-- 10 files changed, 106 insertions(+), 65 deletions(-) delete mode 100644 .codeclimate.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index ef7e1cb56..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: - pep8: - enabled: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..46df6809d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Publish Python distributions +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.6' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + notify-on-failure: + name: Slack notify on failure + if: ${{ failure() }} + needs: [ release ] + runs-on: ubuntu-latest + steps: + - uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: 'danger' + SLACK_ICON_EMOJI: ':github:' + SLACK_MESSAGE: ${{ format('Failed to release {1}{3} {0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id, ':') }} + SLACK_TITLE: Release Failure + SLACK_USERNAME: GitHub Actions + SLACK_MSG_AUTHOR: twilio-dx + SLACK_FOOTER: Posted automatically using GitHub Actions + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + MSG_MINIMAL: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..fe2015a6e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: Run Tests +on: + push: + branches: [ '*' ] + pull_request: + branches: [ main ] + schedule: + # Run automatically at 8AM PST Monday-Friday + - cron: '0 15 * * 1-5' + workflow_dispatch: + +jobs: + tests: + name: Run Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Build & Test + run: make test-docker test-install + + notify-on-failure: + name: Slack notify on failure + if: ${{ failure() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} + needs: [ tests ] + runs-on: ubuntu-latest + steps: + - uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: 'danger' + SLACK_ICON_EMOJI: ':github:' + SLACK_MESSAGE: ${{ format('Failed running build on {1}{3} {0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id, ':') }} + SLACK_TITLE: Build Failure + SLACK_USERNAME: GitHub Actions + SLACK_MSG_AUTHOR: twilio-dx + SLACK_FOOTER: Posted automatically using GitHub Actions + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + MSG_MINIMAL: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 46f3601b3..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -dist: xenial -language: python -cache: pip -services: -- docker -env: - matrix: - - version=2.7 - - version=3.5 - - version=3.6 - - version=3.7 - - version=3.8 - - version=3.9 - global: - - CC_TEST_REPORTER_ID=$TRAVIS_CODE_CLIMATE_TOKEN -before_script: -- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 - > ./cc-test-reporter -- chmod +x ./cc-test-reporter -- "./cc-test-reporter before-build" -script: -- if [[ "$TRAVIS_BRANCH" == "main" || "$TRAVIS_BRANCH" == "travis" ]] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin; - fi -- make test-docker -after_script: -- make test-install -- ". venv/bin/activate; codecov" -- "./cc-test-reporter after-build --exit-code $?" -deploy: - provider: pypi - user: __token__ - password: "$PYPI_TOKEN" - skip_cleanup: true - distributions: sdist bdist_wheel - on: - tags: true - condition: "$version = 3.6" -notifications: - slack: - if: branch = main - on_pull_requests: false - on_success: never - on_failure: change - rooms: - secure: B0SJHc9Syyf5HOl21abg/Uj/Gp7EusCOly/2JZzUUHCWtxC8C9pWfGf2e674R4vdeJ3FmTKz/1jJZ96vzV0z+XUpT2Fnn6URi4kjI8C0XNTs8la+bz5riSM4TokYOv0HGbL/r0OmHraodCxuX1rpkcYX+FD1dwcGC70eEB6NIu4= diff --git a/Makefile b/Makefile index 620a25993..bb22db4df 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ venv: clean @python --version || (echo "Python is not installed, please install Python 2 or Python 3"; exit 1); + pip install virtualenv virtualenv --python=python venv install: venv diff --git a/README.md b/README.md index 8ce206abb..e2208293d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ ![SendGrid Logo](twilio_sendgrid_logo.png) -[![Travis Badge](https://travis-ci.com/sendgrid/sendgrid-python.svg?branch=main)](https://travis-ci.com/sendgrid/sendgrid-python) -[![codecov](https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/main.svg?style=flat-square&label=Codecov+Coverage)](https://codecov.io/gh/sendgrid/sendgrid-python) +[![Tests](https://github.com/sendgrid/sendgrid-python/actions/workflows/tests.yml/badge.svg)](https://github.com/sendgrid/sendgrid-python/actions/workflows/tests.yml) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) diff --git a/README.rst b/README.rst index a7a076c22..59f693f0f 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ -|Travis Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |Email Notifications Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| +|Tests Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |Email Notifications Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| **This library allows you to quickly and easily use the Twilio SendGrid Web API v3 via Python.** @@ -288,8 +288,8 @@ License .. _troubleshooting guide: https://github.com/sendgrid/sendgrid-python/blob/HEAD/TROUBLESHOOTING.md .. _The MIT License (MIT): https://github.com/sendgrid/sendgrid-python/blob/HEAD/LICENSE -.. |Travis Badge| image:: https://travis-ci.com/sendgrid/sendgrid-python.svg?branch=main - :target: https://travis-ci.com/sendgrid/sendgrid-python +.. |Tests Badge| image:: https://github.com/sendgrid/sendgrid-python/actions/workflows/test.yml/badge.svg + :target: https://github.com/sendgrid/sendgrid-python/actions/workflows/test.yml .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/sendgrid.svg :target: https://pypi.org/project/sendgrid/ .. |PyPI Version| image:: https://img.shields.io/pypi/v/sendgrid.svg diff --git a/requirements.txt b/requirements.txt index b2ba4d1be..f4b4ba105 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Flask==1.0.2 +Flask==1.1.2 PyYAML>=4.2b1 python-http-client>=3.2.1 six==1.11.0 diff --git a/test/test_project.py b/test/test_project.py index e30049a3f..c78293dff 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -11,14 +11,6 @@ def test_env(self): def test_gitignore(self): self.assertTrue(os.path.isfile('./.gitignore')) - # ./.travis.yml - def test_travis(self): - self.assertTrue(os.path.isfile('./.travis.yml')) - - # ./.codeclimate.yml - def test_codeclimate(self): - self.assertTrue(os.path.isfile('./.codeclimate.yml')) - # ./CHANGELOG.md def test_changelog(self): self.assertTrue(os.path.isfile('./CHANGELOG.md')) diff --git a/test/test_sendgrid.py b/test/test_sendgrid.py index f6177a7d5..0c63851eb 100644 --- a/test/test_sendgrid.py +++ b/test/test_sendgrid.py @@ -1649,14 +1649,14 @@ def test_suppression_invalid_emails__email__delete(self): def test_suppression_spam_report__email__get(self): email = "test_url_param" headers = {'X-Mock': 200} - response = self.sg.client.suppression.spam_report._( + response = self.sg.client.suppression.spam_reports._( email).get(request_headers=headers) self.assertEqual(response.status_code, 200) def test_suppression_spam_report__email__delete(self): email = "test_url_param" headers = {'X-Mock': 204} - response = self.sg.client.suppression.spam_report._( + response = self.sg.client.suppression.spam_reports._( email).delete(request_headers=headers) self.assertEqual(response.status_code, 204) From 807b08bb6dff27e84e454f19f31b2a15a9aa3c9e Mon Sep 17 00:00:00 2001 From: Jennifer Mah Date: Wed, 1 Dec 2021 10:49:09 -0800 Subject: [PATCH 076/149] fix: add Python distribution to release GH action --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46df6809d..2c59e9075 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ jobs: run: | python -m pip install --upgrade pip pip install build + python setup.py sdist bdist_wheel - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From cec16a4c8941bd778a3c2a432e058d0b7df53dc4 Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:21:17 -0800 Subject: [PATCH 077/149] chore: add wheel install for GH action release --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c59e9075..65ef25a39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,7 @@ jobs: run: | python -m pip install --upgrade pip pip install build + pip install wheel python setup.py sdist bdist_wheel - name: Publish package to PyPI From e00c300aeb3d1031aca96463cab11b4f541c1642 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 1 Dec 2021 21:10:29 +0000 Subject: [PATCH 078/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35318143e..4f102be33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-12-01] Version 6.9.2 +-------------------------- +**Library - Chore** +- [PR #1027](https://github.com/sendgrid/sendgrid-python/pull/1027): migrate to GitHub Actions. Thanks to [@JenniferMah](https://github.com/JenniferMah)! + + [2021-11-17] Version 6.9.1 -------------------------- **Library - Chore** From ed0b3ff73dfcb3b8188199106b7730f7b1d69f16 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 1 Dec 2021 21:13:05 +0000 Subject: [PATCH 079/149] Release 6.9.2 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index ae7f14541..b8fc882cc 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.1' +__version__ = '6.9.2' From 1dcc378bb90ed0d043c9dcfd239eb4c0b5501a81 Mon Sep 17 00:00:00 2001 From: childish-sambino Date: Tue, 14 Dec 2021 09:02:41 -0600 Subject: [PATCH 080/149] test: split up unit and integ tests (#1029) --- Makefile | 3 ++- test/integ/__init__.py | 0 test/{ => integ}/test_sendgrid.py | 0 test/unit/__init__.py | 0 test/{ => unit}/test_app.py | 0 test/{ => unit}/test_config.py | 0 test/{ => unit}/test_email.py | 0 test/{ => unit}/test_eventwebhook.py | 0 test/{ => unit}/test_inbound_send.py | 0 test/{ => unit}/test_mail_helpers.py | 0 test/{ => unit}/test_parse.py | 0 test/{ => unit}/test_project.py | 0 test/{ => unit}/test_spam_check.py | 0 test/{ => unit}/test_stats.py | 0 test/{ => unit}/test_twilio_email.py | 0 test/{ => unit}/test_unassigned.py | 0 16 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 test/integ/__init__.py rename test/{ => integ}/test_sendgrid.py (100%) create mode 100644 test/unit/__init__.py rename test/{ => unit}/test_app.py (100%) rename test/{ => unit}/test_config.py (100%) rename test/{ => unit}/test_email.py (100%) rename test/{ => unit}/test_eventwebhook.py (100%) rename test/{ => unit}/test_inbound_send.py (100%) rename test/{ => unit}/test_mail_helpers.py (100%) rename test/{ => unit}/test_parse.py (100%) rename test/{ => unit}/test_project.py (100%) rename test/{ => unit}/test_spam_check.py (100%) rename test/{ => unit}/test_stats.py (100%) rename test/{ => unit}/test_twilio_email.py (100%) rename test/{ => unit}/test_unassigned.py (100%) diff --git a/Makefile b/Makefile index bb22db4df..fb61004ce 100644 --- a/Makefile +++ b/Makefile @@ -13,9 +13,10 @@ test-install: install . venv/bin/activate; pip install -r test/requirements.txt test: test-install + . venv/bin/activate; coverage run -m unittest discover -s test/unit test-integ: test - . venv/bin/activate; coverage run -m unittest discover + . venv/bin/activate; coverage run -m unittest discover -s test/integ version ?= latest test-docker: diff --git a/test/integ/__init__.py b/test/integ/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_sendgrid.py b/test/integ/test_sendgrid.py similarity index 100% rename from test/test_sendgrid.py rename to test/integ/test_sendgrid.py diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_app.py b/test/unit/test_app.py similarity index 100% rename from test/test_app.py rename to test/unit/test_app.py diff --git a/test/test_config.py b/test/unit/test_config.py similarity index 100% rename from test/test_config.py rename to test/unit/test_config.py diff --git a/test/test_email.py b/test/unit/test_email.py similarity index 100% rename from test/test_email.py rename to test/unit/test_email.py diff --git a/test/test_eventwebhook.py b/test/unit/test_eventwebhook.py similarity index 100% rename from test/test_eventwebhook.py rename to test/unit/test_eventwebhook.py diff --git a/test/test_inbound_send.py b/test/unit/test_inbound_send.py similarity index 100% rename from test/test_inbound_send.py rename to test/unit/test_inbound_send.py diff --git a/test/test_mail_helpers.py b/test/unit/test_mail_helpers.py similarity index 100% rename from test/test_mail_helpers.py rename to test/unit/test_mail_helpers.py diff --git a/test/test_parse.py b/test/unit/test_parse.py similarity index 100% rename from test/test_parse.py rename to test/unit/test_parse.py diff --git a/test/test_project.py b/test/unit/test_project.py similarity index 100% rename from test/test_project.py rename to test/unit/test_project.py diff --git a/test/test_spam_check.py b/test/unit/test_spam_check.py similarity index 100% rename from test/test_spam_check.py rename to test/unit/test_spam_check.py diff --git a/test/test_stats.py b/test/unit/test_stats.py similarity index 100% rename from test/test_stats.py rename to test/unit/test_stats.py diff --git a/test/test_twilio_email.py b/test/unit/test_twilio_email.py similarity index 100% rename from test/test_twilio_email.py rename to test/unit/test_twilio_email.py diff --git a/test/test_unassigned.py b/test/unit/test_unassigned.py similarity index 100% rename from test/test_unassigned.py rename to test/unit/test_unassigned.py From 54fb4035865bee25dea8bbbcd7ea0d9a9fa43235 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 15 Dec 2021 19:39:08 +0000 Subject: [PATCH 081/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f102be33..d4f521a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2021-12-15] Version 6.9.3 +-------------------------- +**Library - Test** +- [PR #1029](https://github.com/sendgrid/sendgrid-python/pull/1029): split up unit and integ tests. Thanks to [@childish-sambino](https://github.com/childish-sambino)! + + [2021-12-01] Version 6.9.2 -------------------------- **Library - Chore** From 1c7493cbadaff93a9b1dd918c63b22e129719f17 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 15 Dec 2021 19:41:42 +0000 Subject: [PATCH 082/149] Release 6.9.3 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index b8fc882cc..9d3fb5211 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.2' +__version__ = '6.9.3' From 76c5f46c103533a1c0d8d857e62f3af915cd4a89 Mon Sep 17 00:00:00 2001 From: Jennifer Mah <42650198+JenniferMah@users.noreply.github.com> Date: Wed, 5 Jan 2022 08:27:25 -0700 Subject: [PATCH 083/149] chore: update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index e5439a92d..5db04ff6d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2021, Twilio SendGrid, Inc. +Copyright (C) 2022, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From b729f401dc9c15393cc0e3036a025deff3d711c6 Mon Sep 17 00:00:00 2001 From: Elmer Thomas Date: Wed, 5 Jan 2022 14:48:25 -0800 Subject: [PATCH 084/149] docs: remove leading spaces on error handling example (#1032) --- use_cases/error_handling.md | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/use_cases/error_handling.md b/use_cases/error_handling.md index 187703b60..d0bdf0945 100644 --- a/use_cases/error_handling.md +++ b/use_cases/error_handling.md @@ -6,24 +6,24 @@ Please see [here](https://github.com/sendgrid/python-http-client/blob/HEAD/pytho There are also email specific exceptions located [here](../sendgrid/helpers/mail/exceptions.py) ```python - import os - from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import (From, To, Subject, PlainTextContent, HtmlContent, Mail) - from python_http_client import exceptions +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import (From, To, Subject, PlainTextContent, HtmlContent, Mail) +from python_http_client import exceptions - sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) - from_email = From("help@twilio.com") - to_email = To("ethomas@twilio.com") - subject = Subject("Sending with Twilio 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, to_email, subject, plain_text_content, html_content) - try: - response = sendgrid_client.send(message) - print(response.status_code) - print(response.body) - print(response.headers) - except exceptions.BadRequestsError as e: - print(e.body) - exit() +sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) +from_email = From("help@twilio.com") +to_email = To("ethomas@twilio.com") +subject = Subject("Sending with Twilio 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, to_email, subject, plain_text_content, html_content) +try: + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except exceptions.BadRequestsError as e: + print(e.body) + exit() ``` From df13b78b0cdcb410b4516f6761c4d3138edd4b2d Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 8 Jan 2022 02:49:23 +0530 Subject: [PATCH 085/149] chore: Remove unused import from distutils (#1031) Co-authored-by: Jennifer Mah --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 365ebb50b..e1d6600d6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ import io import os -from distutils.file_util import copy_file from setuptools import setup, find_packages From ba7135ea3c41785efc1f1177faa3b287a02f721c Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 12 Jan 2022 20:32:31 +0000 Subject: [PATCH 086/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f521a85..817ee5611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2022-01-12] Version 6.9.4 +-------------------------- +**Library - Chore** +- [PR #1031](https://github.com/sendgrid/sendgrid-python/pull/1031): Remove unused import from distutils. Thanks to [@tirkarthi](https://github.com/tirkarthi)! + +**Library - Docs** +- [PR #1032](https://github.com/sendgrid/sendgrid-python/pull/1032): remove leading spaces on error handling example. Thanks to [@thinkingserious](https://github.com/thinkingserious)! + + [2021-12-15] Version 6.9.3 -------------------------- **Library - Test** From dc78d9ebf635d8e5e9d20439d51c6d9f3965b204 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 12 Jan 2022 20:34:59 +0000 Subject: [PATCH 087/149] Release 6.9.4 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 9d3fb5211..55b51e8b2 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.3' +__version__ = '6.9.4' From d7f81db48bdeb6ff14b263604ce3d6036284a232 Mon Sep 17 00:00:00 2001 From: Jack Slingerland Date: Thu, 20 Jan 2022 20:04:47 -0500 Subject: [PATCH 088/149] docs: Removing unused json import (#1036) --- use_cases/transactional_templates.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/use_cases/transactional_templates.md b/use_cases/transactional_templates.md index 9ee4848c3..460fd65ee 100644 --- a/use_cases/transactional_templates.md +++ b/use_cases/transactional_templates.md @@ -28,7 +28,6 @@ I hope you are having a great day in {{ city }} :) ```python import os -import json from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail @@ -86,7 +85,6 @@ I hope you are having a great day in {{{ city }}} :) ```python import os -import json from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail From d742ada8e6aaee77ab2f4f731f7099372255948b Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 26 Jan 2022 19:23:42 +0000 Subject: [PATCH 089/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 817ee5611..5e7d5c0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2022-01-26] Version 6.9.5 +-------------------------- +**Library - Docs** +- [PR #1036](https://github.com/sendgrid/sendgrid-python/pull/1036): Removing unused json import. Thanks to [@vital101](https://github.com/vital101)! + + [2022-01-12] Version 6.9.4 -------------------------- **Library - Chore** From 3b17a349cc45eeb796d038722cd4a33ed4dea3ed Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 26 Jan 2022 19:26:42 +0000 Subject: [PATCH 090/149] Release 6.9.5 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 55b51e8b2..5b3835adf 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.4' +__version__ = '6.9.5' From bbd1319d5719728de5908b0fc650a5083ed2dcac Mon Sep 17 00:00:00 2001 From: Hunga1 Date: Thu, 3 Feb 2022 13:26:45 -0700 Subject: [PATCH 091/149] chore: merge test and deploy workflows (#1039) --- .github/workflows/release.yml | 50 ------------------- .github/workflows/test-and-deploy.yml | 71 +++++++++++++++++++++++++++ .github/workflows/tests.yml | 50 ------------------- README.md | 2 +- 4 files changed, 72 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test-and-deploy.yml delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 65ef25a39..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Publish Python distributions -on: - push: - tags: - - '*' - workflow_dispatch: - -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout sendgrid-python - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.6' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - pip install wheel - python setup.py sdist bdist_wheel - - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} - - notify-on-failure: - name: Slack notify on failure - if: ${{ failure() }} - needs: [ release ] - runs-on: ubuntu-latest - steps: - - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_COLOR: 'danger' - SLACK_ICON_EMOJI: ':github:' - SLACK_MESSAGE: ${{ format('Failed to release {1}{3} {0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id, ':') }} - SLACK_TITLE: Release Failure - SLACK_USERNAME: GitHub Actions - SLACK_MSG_AUTHOR: twilio-dx - SLACK_FOOTER: Posted automatically using GitHub Actions - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - MSG_MINIMAL: true diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml new file mode 100644 index 000000000..de9233e0a --- /dev/null +++ b/.github/workflows/test-and-deploy.yml @@ -0,0 +1,71 @@ +name: Test and Deploy +on: + push: + branches: [ '*' ] + tags: [ '*' ] + pull_request: + branches: [ main ] + schedule: + # Run automatically at 8AM PST Monday-Friday + - cron: '0 15 * * 1-5' + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v2 + + - name: Build & Test + run: make test-docker version=${{ matrix.python-version }} + + deploy: + name: Deploy + if: success() && github.ref_type == 'tag' + needs: [ test ] + runs-on: ubuntu-latest + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.6' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install wheel + python setup.py sdist bdist_wheel + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + + notify-on-failure: + name: Slack notify on failure + if: failure() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') + needs: [ test, deploy ] + runs-on: ubuntu-latest + steps: + - uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: failure + SLACK_ICON_EMOJI: ':github:' + SLACK_MESSAGE: ${{ format('Test *{0}*, Deploy *{1}*, {2}/{3}/actions/runs/{4}', needs.test.result, needs.deploy.result, github.server_url, github.repository, github.run_id) }} + SLACK_TITLE: Action Failure - ${{ github.repository }} + SLACK_USERNAME: GitHub Actions + SLACK_MSG_AUTHOR: twilio-dx + SLACK_FOOTER: Posted automatically using GitHub Actions + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + MSG_MINIMAL: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index fe2015a6e..000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Run Tests -on: - push: - branches: [ '*' ] - pull_request: - branches: [ main ] - schedule: - # Run automatically at 8AM PST Monday-Friday - - cron: '0 15 * * 1-5' - workflow_dispatch: - -jobs: - tests: - name: Run Tests - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] - steps: - - name: Checkout sendgrid-python - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Build & Test - run: make test-docker test-install - - notify-on-failure: - name: Slack notify on failure - if: ${{ failure() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} - needs: [ tests ] - runs-on: ubuntu-latest - steps: - - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_COLOR: 'danger' - SLACK_ICON_EMOJI: ':github:' - SLACK_MESSAGE: ${{ format('Failed running build on {1}{3} {0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id, ':') }} - SLACK_TITLE: Build Failure - SLACK_USERNAME: GitHub Actions - SLACK_MSG_AUTHOR: twilio-dx - SLACK_FOOTER: Posted automatically using GitHub Actions - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - MSG_MINIMAL: true diff --git a/README.md b/README.md index e2208293d..a653e38f9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![SendGrid Logo](twilio_sendgrid_logo.png) -[![Tests](https://github.com/sendgrid/sendgrid-python/actions/workflows/tests.yml/badge.svg)](https://github.com/sendgrid/sendgrid-python/actions/workflows/tests.yml) +[![BuildStatus](https://github.com/sendgrid/sendgrid-python/actions/workflows/test-and-deploy.yml/badge.svg)](https://github.com/sendgrid/sendgrid-python/actions/workflows/test-and-deploy.yml) [![Docker Badge](https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg)](https://hub.docker.com/r/sendgrid/sendgrid-python/) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow)](https://twitter.com/sendgrid) From 0c7a723f0b7c19a5fb5535c2d006d87191f355f9 Mon Sep 17 00:00:00 2001 From: Shwetha Radhakrishna Date: Thu, 3 Feb 2022 16:59:38 -0600 Subject: [PATCH 092/149] chore: add gh release to workflow (#1041) --- .github/workflows/test-and-deploy.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index de9233e0a..f061a5096 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -46,6 +46,13 @@ jobs: pip install wheel python setup.py sdist bdist_wheel + - name: Create GitHub Release + uses: sendgrid/dx-automator/actions/release@main + with: + footer: '**[pypi](https://pypi.org/project/sendgrid/${version})**' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: From 412ca9914eec30866a44f1c01f008df3c0a95f28 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Mon, 7 Feb 2022 12:50:41 -0600 Subject: [PATCH 093/149] fix: only do a Docker Login if the secrets are available --- .github/workflows/test-and-deploy.yml | 65 +++++++++++++++------------ 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index f061a5096..7ce3240c7 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -18,46 +18,55 @@ jobs: strategy: matrix: python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] + env: + DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: - name: Checkout sendgrid-python uses: actions/checkout@v2 + - name: Login to Docker Hub + if: env.DOCKER_LOGIN + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_AUTH_TOKEN }} + - name: Build & Test run: make test-docker version=${{ matrix.python-version }} deploy: - name: Deploy - if: success() && github.ref_type == 'tag' - needs: [ test ] - runs-on: ubuntu-latest - steps: - - name: Checkout sendgrid-python - uses: actions/checkout@v2 + name: Deploy + if: success() && github.ref_type == 'tag' + needs: [ test ] + runs-on: ubuntu-latest + steps: + - name: Checkout sendgrid-python + uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.6' + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.6' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - pip install wheel - python setup.py sdist bdist_wheel + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install wheel + python setup.py sdist bdist_wheel - - name: Create GitHub Release - uses: sendgrid/dx-automator/actions/release@main - with: - footer: '**[pypi](https://pypi.org/project/sendgrid/${version})**' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Create GitHub Release + uses: sendgrid/dx-automator/actions/release@main + with: + footer: '**[pypi](https://pypi.org/project/sendgrid/${version})**' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} notify-on-failure: name: Slack notify on failure From 807a2c97dcc9fc92764f97c73b6086ab2426c4f5 Mon Sep 17 00:00:00 2001 From: childish-sambino Date: Tue, 8 Feb 2022 09:14:53 -0600 Subject: [PATCH 094/149] chore: upgrade supported language versions (#1043) --- .github/workflows/test-and-deploy.yml | 4 ++-- setup.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 7ce3240c7..842277933 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9' ] + python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10' ] env: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: @@ -46,7 +46,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.6' + python-version: '3.10' - name: Install dependencies run: | diff --git a/setup.py b/setup.py index e1d6600d6..7e84802f4 100644 --- a/setup.py +++ b/setup.py @@ -38,5 +38,8 @@ def getRequires(): 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ] ) From b85c8fed2ffeff2084d005c164bd13851f245db7 Mon Sep 17 00:00:00 2001 From: childish-sambino Date: Wed, 9 Feb 2022 10:56:42 -0600 Subject: [PATCH 095/149] chore: drop pytest which was not being used (#1044) --- requirements.txt | 1 - test/unit/test_unassigned.py | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index f4b4ba105..0c34aafd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,5 @@ Flask==1.1.2 PyYAML>=4.2b1 python-http-client>=3.2.1 six==1.11.0 -pytest==3.8.2 starkbank-ecdsa>=2.0.1 more-itertools==5.0.0 diff --git a/test/unit/test_unassigned.py b/test/unit/test_unassigned.py index 6054447d8..08ab943bb 100644 --- a/test/unit/test_unassigned.py +++ b/test/unit/test_unassigned.py @@ -1,9 +1,7 @@ import json -import pytest from sendgrid.helpers.endpoints.ip.unassigned import unassigned - ret_json = '''[ { "ip": "167.89.21.3", "pools": [ @@ -69,8 +67,7 @@ def make_data(): def test_unassigned_ip_json(): - - data = make_data() + data = make_data() as_json = True calculated = unassigned(get_all_ip(), as_json=as_json) @@ -81,8 +78,7 @@ def test_unassigned_ip_json(): def test_unassigned_ip_obj(): - - data = make_data() + data = make_data() as_json = False calculated = unassigned(get_all_ip(), as_json=as_json) From 78615fcd2db479d2d06580fe35739539d68a285a Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 9 Feb 2022 14:49:27 -0800 Subject: [PATCH 096/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7d5c0be..f009359c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2022-02-09] Version 6.9.6 +-------------------------- +**Library - Chore** +- [PR #1044](https://github.com/sendgrid/sendgrid-python/pull/1044): drop pytest which was not being used. Thanks to [@childish-sambino](https://github.com/childish-sambino)! +- [PR #1043](https://github.com/sendgrid/sendgrid-python/pull/1043): upgrade supported language versions. Thanks to [@childish-sambino](https://github.com/childish-sambino)! +- [PR #1041](https://github.com/sendgrid/sendgrid-python/pull/1041): add gh release to workflow. Thanks to [@shwetha-manvinkurke](https://github.com/shwetha-manvinkurke)! +- [PR #1039](https://github.com/sendgrid/sendgrid-python/pull/1039): merge test and deploy workflows. Thanks to [@Hunga1](https://github.com/Hunga1)! + + [2022-01-26] Version 6.9.5 -------------------------- **Library - Docs** From f91a2162add712658b9f5e201f06c51a842cbd0a Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 9 Feb 2022 14:49:28 -0800 Subject: [PATCH 097/149] Release 6.9.6 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 5b3835adf..eea67ac10 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.5' +__version__ = '6.9.6' From 082e3c57c7da4649018b36550f8512c4d99d0fd8 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Mon, 28 Feb 2022 15:42:42 -0800 Subject: [PATCH 098/149] chore: fix flask dependency test issues (#1050) --- test/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/requirements.txt b/test/requirements.txt index 6cb2e96d2..17302b22e 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,8 @@ pyyaml -flask +Flask==1.1.4 six coverage codecov mock +itsdangerous==1.1.0 +markupsafe==1.1.1 From 0b774384fb721196b12870d108f7bafe7c8c6b40 Mon Sep 17 00:00:00 2001 From: Elise Shanholtz Date: Tue, 1 Mar 2022 09:53:43 -0800 Subject: [PATCH 099/149] chore: push Datadog Release Metric upon deploy success (#1049) --- .github/workflows/test-and-deploy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 842277933..ac0bca0a8 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -68,6 +68,11 @@ jobs: user: __token__ password: ${{ secrets.PYPI_TOKEN }} + - name: Submit metric to Datadog + uses: sendgrid/dx-automator/actions/datadog-release-metric@main + env: + DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} + notify-on-failure: name: Slack notify on failure if: failure() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref_type == 'tag') From d7d292cf2241c18d052b8e3705dcc134cc0912d5 Mon Sep 17 00:00:00 2001 From: Vinicius Mesel Date: Fri, 4 Mar 2022 18:29:35 -0300 Subject: [PATCH 100/149] chore: Update mail_example.py (#1048) Co-authored-by: Elise Shanholtz --- examples/helpers/mail_example.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/helpers/mail_example.py b/examples/helpers/mail_example.py index 384181501..f6905787b 100644 --- a/examples/helpers/mail_example.py +++ b/examples/helpers/mail_example.py @@ -1,3 +1,6 @@ +import os +import json + from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import * @@ -8,11 +11,7 @@ def build_hello_email(): ## Send a Single Email to a Single Recipient - import os - import json - from sendgrid import SendGridAPIClient - from sendgrid.helpers.mail import Mail, From, To, Subject, PlainTextContent, HtmlContent, SendGridException - + message = Mail(from_email=From('from@example.com', 'Example From Name'), to_emails=To('to@example.com', 'Example To Name'), subject=Subject('Sending with SendGrid is Fun'), @@ -84,11 +83,8 @@ def get_mock_personalization_dict(): return mock_pers def build_multiple_emails_personalized(): - import json - from sendgrid.helpers.mail import Mail, From, To, Cc, Bcc, Subject, PlainTextContent, \ - HtmlContent, SendGridException, Personalization - # Note that the domain for all From email addresses must match + message = Mail(from_email=From('from@example.com', 'Example From Name'), subject=Subject('Sending with SendGrid is Fun'), plain_text_content=PlainTextContent('and easy to do anywhere, even with Python'), @@ -117,6 +113,7 @@ def build_multiple_emails_personalized(): def build_attachment1(): """Build attachment mock. Make sure your content is base64 encoded before passing into attachment.content. Another example: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/attachment.md""" + attachment = Attachment() attachment.file_content = ("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNl" "Y3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQ3JhcyBwdW12") From d1a54500ac460fb9e392701925b1d9f21834581b Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 9 Mar 2022 12:21:55 -0800 Subject: [PATCH 101/149] [Librarian] Version Bump --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f009359c4..a9d8f79db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log All notable changes to this project will be documented in this file. +[2022-03-09] Version 6.9.7 +-------------------------- +**Library - Chore** +- [PR #1048](https://github.com/sendgrid/sendgrid-python/pull/1048): Update mail_example.py. Thanks to [@vmesel](https://github.com/vmesel)! +- [PR #1049](https://github.com/sendgrid/sendgrid-python/pull/1049): push Datadog Release Metric upon deploy success. Thanks to [@eshanholtz](https://github.com/eshanholtz)! +- [PR #1050](https://github.com/sendgrid/sendgrid-python/pull/1050): fix flask dependency test issues. Thanks to [@eshanholtz](https://github.com/eshanholtz)! + + [2022-02-09] Version 6.9.6 -------------------------- **Library - Chore** From 202fabc99073f1a61302d0e8ecb143e0d45bd8ce Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 9 Mar 2022 12:21:55 -0800 Subject: [PATCH 102/149] Release 6.9.7 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index eea67ac10..59b5dedc7 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.6' +__version__ = '6.9.7' From e3d3ce500ba41f20ed6ce8f0b1a7fcb18882d917 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Thu, 24 Mar 2022 10:48:08 -0500 Subject: [PATCH 103/149] chore: remove outdated announcements --- README.md | 6 +----- README.rst | 10 +--------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a653e38f9..321d86ca3 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ [![GitHub contributors](https://img.shields.io/github/contributors/sendgrid/sendgrid-python.svg)](https://github.com/sendgrid/sendgrid-python/graphs/contributors) [![Open Source Helpers](https://www.codetriage.com/sendgrid/sendgrid-python/badges/users.svg)](https://www.codetriage.com/sendgrid/sendgrid-python) -**The default branch name for this repository has been changed to `main` as of 07/27/2020.** - **This library allows you to quickly and easily use the SendGrid Web API v3 via Python.** Version 3.X.X+ of this library provides full support for all SendGrid [Web API v3](https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html) endpoints, including the new [v3 /mail/send](https://sendgrid.com/blog/introducing-v3mailsend-sendgrids-new-mail-endpoint). @@ -187,9 +185,7 @@ Please see [our helper](sendgrid/helpers/inbound) for utilizing our Inbound Pars # Announcements -Please see our announcement regarding [breaking changes](https://github.com/sendgrid/sendgrid-python/issues/217). Your support is appreciated! - -All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). You may also subscribe to email [release notifications](https://dx.sendgrid.com/newsletter/java) for releases and breaking changes. +All updates to this library are documented in our [CHANGELOG](CHANGELOG.md) and [releases](https://github.com/sendgrid/sendgrid-python/releases). # How to Contribute diff --git a/README.rst b/README.rst index 59f693f0f..93062ba66 100644 --- a/README.rst +++ b/README.rst @@ -3,15 +3,12 @@ -|Tests Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |Email Notifications Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| +|Tests Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| **This library allows you to quickly and easily use the Twilio SendGrid Web API v3 via Python.** **NEW:** -**The default branch name for this repository has been changed to `main` as of 07/27/2020.** - -- Subscribe to email `notifications`_ for releases and breaking changes. - Version 6.X release is a BREAKING CHANGE from version 5.X, please see the `release notes`_ for details. - Send SMS messages with `Twilio`_. @@ -223,7 +220,6 @@ Announcements ============= All updates to this library are documented in our `CHANGELOG`_ and `releases`_. -You may also subscribe to email `release notifications`_ for releases and breaking changes. How to Contribute ================= @@ -253,7 +249,6 @@ License `The MIT License (MIT)`_ -.. _notifications: https://dx.sendgrid.com/newsletter/python .. _Twilio: https://github.com/sendgrid/sendgrid-python/blob/HEAD/use_cases/sms.md .. _release notes: https://github.com/sendgrid/sendgrid-python/releases/tag/v6.0.0 .. _Web API v3: https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html @@ -279,7 +274,6 @@ License .. _breaking changes: https://github.com/sendgrid/sendgrid-python/issues/217 .. _CHANGELOG: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CHANGELOG.md .. _releases: https://github.com/sendgrid/sendgrid-python/releases -.. _release notifications: https://dx.sendgrid.com/newsletter/python .. _CONTRIBUTING: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md .. _Feature Request: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#feature-request .. _Bug Reports: https://github.com/sendgrid/sendgrid-python/blob/HEAD/CONTRIBUTING.md#submit-a-bug-report @@ -298,8 +292,6 @@ License :target: https://codecov.io/gh/sendgrid/sendgrid-python .. |Docker Badge| image:: https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg :target: https://hub.docker.com/r/sendgrid/sendgrid-python/ -.. |Email Notifications Badge| image:: https://dx.sendgrid.com/badge/python - :target: https://dx.sendgrid.com/newsletter/python .. |MIT licensed| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: ./LICENSE .. |Twitter Follow| image:: https://img.shields.io/twitter/follow/sendgrid.svg?style=social&label=Follow From 27843fe2d5b7fdf188f606ef20f2beab32b97eee Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Fri, 25 Mar 2022 13:39:50 -0500 Subject: [PATCH 104/149] feat: add PR title validation --- .github/workflows/pr-lint.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/pr-lint.yml diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 000000000..8388f21e8 --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,15 @@ +name: Lint PR +on: + pull_request_target: + types: [ opened, edited, reopened ] + +jobs: + validate: + name: Validate title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4 + with: + types: chore docs fix feat test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 903c9cb87cf87d018a1d131b4903b4a300b92e0a Mon Sep 17 00:00:00 2001 From: Jonathan Berger Date: Tue, 29 Mar 2022 08:04:42 -0700 Subject: [PATCH 105/149] docs: Fix link that has drifted (#1052) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 321d86ca3..9ffdbb7d0 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ print(response.body) print(response.headers) ``` -The `Mail` constructor creates a [personalization object](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html) for you. [Here](examples/helpers/mail_example.py#L16) is an example of how to add it. +The `Mail` constructor creates a [personalization object](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html) for you. [Here](examples/helpers/mail_example.py#L28) is an example of how to add it. ### Without Mail Helper Class From 43c03bdd8baca80a80382bd6445a7c6021d697fa Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Thu, 21 Apr 2022 14:44:33 -0500 Subject: [PATCH 106/149] test: lint PRs on synchronize events Since synchronize events clears the status checks, it needs to be re-run. --- .github/workflows/pr-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 8388f21e8..dc7af3d3c 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -1,7 +1,7 @@ name: Lint PR on: pull_request_target: - types: [ opened, edited, reopened ] + types: [ opened, edited, synchronize, reopened ] jobs: validate: From c798252a31949b9b263f9722294eb94ba3bb1e33 Mon Sep 17 00:00:00 2001 From: "Gareth Paul Jones (GPJ)" Date: Mon, 9 May 2022 10:03:43 -0700 Subject: [PATCH 107/149] docs: Modify README.md in alignment with SendGrid Support (#1055) --- .github/ISSUE_TEMPLATE/config.yml | 10 ---------- ISSUE_TEMPLATE.md | 30 ------------------------------ PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 10 ++++++---- test/unit/test_project.py | 4 ---- 5 files changed, 7 insertions(+), 49 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index afcba3446..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,10 +0,0 @@ -contact_links: - - name: Twilio SendGrid Support - url: https://support.sendgrid.com - about: Get Support - - name: Stack Overflow - url: https://stackoverflow.com/questions/tagged/sendgrid-python+or+sendgrid+python - about: Ask questions on Stack Overflow - - name: Documentation - url: https://sendgrid.com/docs/for-developers/ - about: View Reference Documentation diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index fb2e15cef..000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ - - -### Issue Summary -A summary of the issue and the environment in which it occurs. If suitable, include the steps required to reproduce the bug. Please feel free to include screenshots, screencasts, or code examples. - -### Steps to Reproduce -1. This is the first step -2. This is the second step -3. Further steps, etc. - -### Code Snippet -```python -# paste code here -``` - -### Exception/Log -``` -# paste exception/log here -``` - -### Technical details: -* sendgrid-python version: -* python version: - diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 7c2789ae4..f78ab3918 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -28,4 +28,4 @@ A short description of what this PR does. - [ ] I have added the necessary documentation about the functionality in the appropriate .md file - [ ] I have added inline documentation to the code I modified -If you have questions, please file a [support ticket](https://support.sendgrid.com), or create a GitHub Issue in this repository. +If you have questions, please file a [support ticket](https://support.sendgrid.com). \ No newline at end of file diff --git a/README.md b/README.md index 9ffdbb7d0..663d1e977 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Version 3.X.X+ of this library provides full support for all SendGrid [Web API v This library represents the beginning of a new path for SendGrid. We want this library to be community driven and SendGrid led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/sendgrid/sendgrid-python/issues) and [pull requests](CONTRIBUTING.md) or simply upvote or comment on existing issues or pull requests. -Please browse the rest of this README for further detail. +**If you need help using SendGrid, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com).** -We appreciate your continued support, thank you! +Please browse the rest of this README for further detail. # Table of Contents @@ -28,6 +28,7 @@ We appreciate your continued support, thank you! * [How to Contribute](#contribute) * [Troubleshooting](#troubleshooting) * [About](#about) +* [Support](#support) * [License](#license) @@ -209,9 +210,10 @@ Please see our [troubleshooting guide](TROUBLESHOOTING.md) for common library is sendgrid-python is maintained and funded by Twilio SendGrid, Inc. The names and logos for sendgrid-python are trademarks of Twilio SendGrid, Inc. -If you need help installing or using the library, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com). + +# Support -If you've instead found a bug in the library or would like new features added, go ahead and open issues or pull requests against this repo! +If you need support, please check the [Twilio SendGrid Support Help Center](https://support.sendgrid.com). # License diff --git a/test/unit/test_project.py b/test/unit/test_project.py index c78293dff..40282bdb7 100644 --- a/test/unit/test_project.py +++ b/test/unit/test_project.py @@ -23,10 +23,6 @@ def test_code_of_conduct(self): def test_contributing(self): self.assertTrue(os.path.isfile('./CONTRIBUTING.md')) - # ./ISSUE_TEMPLATE.md - def test_issue_template(self): - self.assertTrue(os.path.isfile('./ISSUE_TEMPLATE.md')) - # ./LICENSE def test_license(self): self.assertTrue(os.path.isfile('./LICENSE')) From 17a7bcc94f3c02f7cf9327139378d1f5e06cac50 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Thu, 12 May 2022 10:28:26 -0500 Subject: [PATCH 108/149] chore: drop the issue links from FIRST_TIMERS doc --- FIRST_TIMERS.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/FIRST_TIMERS.md b/FIRST_TIMERS.md index 528580c34..a1d563a75 100644 --- a/FIRST_TIMERS.md +++ b/FIRST_TIMERS.md @@ -51,29 +51,3 @@ git push origin ## Important notice Before creating a pull request, make sure that you respect the repository's constraints regarding contributions. You can find them in the [CONTRIBUTING.md](CONTRIBUTING.md) file. - -## Repositories with Open, Easy, Help Wanted, Issue Filters - -* [Python SDK](https://github.com/sendgrid/sendgrid-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [PHP SDK](https://github.com/sendgrid/sendgrid-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [C# SDK](https://github.com/sendgrid/sendgrid-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Ruby SDK](https://github.com/sendgrid/sendgrid-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Node.js SDK](https://github.com/sendgrid/sendgrid-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Java SDK](https://github.com/sendgrid/sendgrid-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Go SDK](https://github.com/sendgrid/sendgrid-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Python SMTPAPI Client](https://github.com/sendgrid/smtpapi-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [PHP SMTPAPI Client](https://github.com/sendgrid/smtpapi-php/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [C# SMTPAPI Client](https://github.com/sendgrid/smtpapi-csharp/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Ruby SMTPAPI Client](https://github.com/sendgrid/smtpapi-ruby/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Node.js SMTPAPI Client](https://github.com/sendgrid/smtpapi-nodejs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Java SMTPAPI Client](https://github.com/sendgrid/smtpapi-java/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Go SMTPAPI Client](https://github.com/sendgrid/smtpapi-go/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Python HTTP Client](https://github.com/sendgrid/python-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [PHP HTTP Client](https://github.com/sendgrid/php-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [C# HTTP Client](https://github.com/sendgrid/csharp-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Java HTTP Client](https://github.com/sendgrid/java-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Ruby HTTP Client](https://github.com/sendgrid/ruby-http-client/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Go HTTP Client](https://github.com/sendgrid/rest/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Open API Definition](https://github.com/sendgrid/sendgrid-oai/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [DX Automator](https://github.com/sendgrid/dx-automator/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) -* [Documentation](https://github.com/sendgrid/docs/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22difficulty%3A+easy%22+label%3A%22status%3A+help+wanted%22) From ef1aa7293e4cda85dcba1cba4524d3e85d8e0281 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Mon, 23 May 2022 11:04:22 -0500 Subject: [PATCH 109/149] docs: drop references to ISSUE_TEMPLATE.md Issues are no longer supported so file was previously removed. --- CONTRIBUTING.md | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2769176d..890641059 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,6 @@ Hello! Thank you for choosing to help contribute to one of the Twilio SendGrid o All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. -- [Feature Request](#feature-request) -- [Submit a Bug Report](#submit-a-bug-report) - - [Please use our Bug Report Template](#please-use-our-bug-report-template) - [Improvements to the Codebase](#improvements-to-the-codebase) - [Development Environment](#development-environment) - [Prerequisites](#prerequisites) @@ -19,31 +16,6 @@ All third party contributors acknowledge that any contributions they provide wil There are a few ways to contribute, which we'll enumerate below: -## Feature Request - -If you'd like to make a feature request, please read this section. - -The GitHub issue tracker is the preferred channel for library feature requests, but please respect the following restrictions: - -- Please **search for existing issues** in order to ensure we don't have duplicate bugs/feature requests. -- Please be respectful and considerate of others when commenting on issues - -## Submit a Bug Report - -Note: DO NOT include your credentials in ANY code examples, descriptions, or media you make public. - -A software bug is a demonstrable issue in the code base. In order for us to diagnose the issue and respond as quickly as possible, please add as much detail as possible into your bug report. - -Before you decide to create a new issue, please try the following: - -1. Check the GitHub issues tab if the identified issue has already been reported, if so, please add a +1 to the existing post. -2. Update to the latest version of this code and check if the issue has already been fixed -3. Copy and fill in the Bug Report Template we have provided below - -### Please use our Bug Report Template - -In order to make the process easier, we've included a [sample bug report template](ISSUE_TEMPLATE.md). - ## Improvements to the Codebase We welcome direct contributions to the sendgrid-python code base. Thank you! From 653cc47c54d53d401d65d1debb3cdc1d7905b4a1 Mon Sep 17 00:00:00 2001 From: Raghav Katyal Date: Wed, 6 Jul 2022 16:14:12 -0700 Subject: [PATCH 110/149] Adding misc as PR type (#1058) --- .github/workflows/pr-lint.yml | 2 +- PULL_REQUEST_TEMPLATE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index dc7af3d3c..2f5232b08 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -10,6 +10,6 @@ jobs: steps: - uses: amannn/action-semantic-pull-request@v4 with: - types: chore docs fix feat test + types: chore docs fix feat test misc env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index f78ab3918..f9448a3b1 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -3,7 +3,7 @@ We appreciate the effort for this pull request but before that please make sure Please format the PR title appropriately based on the type of change: [!]: -Where is one of: docs, chore, feat, fix, test. +Where is one of: docs, chore, feat, fix, test, misc. Add a '!' after the type for breaking changes (e.g. feat!: new breaking feature). **All third-party contributors acknowledge that any contributions they provide will be made under the same open-source license that the open-source project is provided under.** From 4288aa618b04a3e99173f59ce43d7a4b379b93c4 Mon Sep 17 00:00:00 2001 From: Sam Harrison Date: Tue, 3 Jan 2023 09:09:32 -0600 Subject: [PATCH 111/149] docs: updated the year in the license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5db04ff6d..3154774a9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2022, Twilio SendGrid, Inc. +Copyright (C) 2023, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 9e8a2599bd86d599465552de591d2ca5413fb101 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 13 Mar 2023 15:46:49 +0100 Subject: [PATCH 112/149] feat: Add Python 3.11 to the testing (#1059) --- .github/workflows/test-and-deploy.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index ac0bca0a8..f14974e43 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10' ] + python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11' ] env: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: diff --git a/setup.py b/setup.py index 7e84802f4..41f11e589 100644 --- a/setup.py +++ b/setup.py @@ -34,12 +34,12 @@ def getRequires(): classifiers=[ 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ] ) From 93612afeb0114cd98b6efd7c82dbcf98c9a4df27 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 13 Mar 2023 15:57:25 +0100 Subject: [PATCH 113/149] misc: Upgrade GitHub Action pr-lint.yml (#1063) --- .github/workflows/pr-lint.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 2f5232b08..31520079c 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -8,8 +8,14 @@ jobs: name: Validate title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v4 + - uses: amannn/action-semantic-pull-request@v5 with: - types: chore docs fix feat test misc + types: | + chore + docs + fix + feat + misc + test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 102842a9dbeb1cabe057ffc0e52603f2dbc44596 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 13 Mar 2023 16:05:34 +0100 Subject: [PATCH 114/149] misc: Upgrade GitHub Action test-and-deploy.yml (#1064) --- .github/workflows/test-and-deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index f14974e43..4db532e3d 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -22,11 +22,11 @@ jobs: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: - name: Checkout sendgrid-python - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Login to Docker Hub if: env.DOCKER_LOGIN - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} @@ -41,10 +41,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sendgrid-python - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.10' From c8076fa684d66ed5c0569feaf7b99dbedd976b77 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 13 Mar 2023 17:11:47 +0100 Subject: [PATCH 115/149] misc: Create GitHub Action to lint Python code (#1065) --- .github/workflows/lint-python.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/lint-python.yml diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml new file mode 100644 index 000000000..1c5f53f23 --- /dev/null +++ b/.github/workflows/lint-python.yml @@ -0,0 +1,16 @@ +# https://beta.ruff.rs +name: ruff +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: pip install --user ruff + - run: ruff --format=github --ignore="E722,F40,F841" --line-length=681 --target-version=py37 . From 4b5c605dda41dbf6dab9ed4ea72ff4014a735885 Mon Sep 17 00:00:00 2001 From: iLan Date: Tue, 21 Mar 2023 11:28:49 -0700 Subject: [PATCH 116/149] feat: Add reply_to_list functionality (#1062) --- sendgrid/helpers/mail/mail.py | 28 ++++++ test/unit/test_mail_helpers.py | 90 +++++++++++++++++++ ..._email_with_multiple_reply_to_addresses.md | 29 ++++++ 3 files changed, 147 insertions(+) create mode 100644 use_cases/send_a_single_email_with_multiple_reply_to_addresses.md diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index ba21f7891..2472ad7d3 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -60,6 +60,7 @@ def __init__( self._ip_pool_name = None self._mail_settings = None self._reply_to = None + self._reply_to_list = None self._send_at = None self._subject = None self._template_id = None @@ -695,6 +696,32 @@ def reply_to(self, value): value = ReplyTo(value[0], value[1]) self._reply_to = value + @property + def reply_to_list(self): + """A list of ReplyTo email addresses + + :rtype: list(ReplyTo), tuple + """ + return self._reply_to_list + + @reply_to_list.setter + def reply_to_list(self, value): + """A list of ReplyTo email addresses + + :param value: A list of ReplyTo email addresses + :type value: list(ReplyTo), tuple + """ + if isinstance(value, list): + for reply in value: + if isinstance(reply, ReplyTo): + if not isinstance(reply.email, str): + raise ValueError('You must provide an email for each entry in a reply_to_list') + else: + raise ValueError( + 'Please use a list of ReplyTos for a reply_to_list.' + ) + self._reply_to_list = value + @property def contents(self): """The contents of the email @@ -981,6 +1008,7 @@ def get(self): '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), + 'reply_to_list': [r.get() for r in self.reply_to_list or []], } return {key: value for key, value in mail.items() diff --git a/test/unit/test_mail_helpers.py b/test/unit/test_mail_helpers.py index a7d08d890..c00307d46 100644 --- a/test/unit/test_mail_helpers.py +++ b/test/unit/test_mail_helpers.py @@ -235,6 +235,58 @@ def test_send_a_single_email_to_multiple_recipients(self): }''') ) + def test_send_a_single_email_with_multiple_reply_to_addresses(self): + from sendgrid.helpers.mail import (Mail, From, ReplyTo, To, Subject, + PlainTextContent, HtmlContent) + self.maxDiff = None + message = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=To('test+to0@example.com', 'Example 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.reply_to_list = [ReplyTo(email = 'test+reply_to_1@example.com'), ReplyTo(email = 'test+reply_to_2@example.com')] + + self.assertEqual( + message.get(), + json.loads(r'''{ + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + }, + { + "type": "text/html", + "value": "and easy to do anywhere, even with Python" + } + ], + "from": { + "email": "test+from@example.com", + "name": "Example From Name" + }, + "personalizations": [ + { + "to": [ + { + "email": "test+to0@example.com", + "name": "Example To Name" + } + ] + } + ], + "reply_to_list": [ + { + "email": "test+reply_to_1@example.com" + }, + { + "email": "test+reply_to_2@example.com" + } + ], + "subject": "Sending with SendGrid is Fun" + }''') + ) + def test_multiple_emails_to_multiple_recipients(self): from sendgrid.helpers.mail import (Mail, From, To, Subject, PlainTextContent, HtmlContent, @@ -568,6 +620,44 @@ def test_value_error_is_raised_on_to_emails_set_to_list_of_lists(self): 'and easy to do anywhere, even with Python'), html_content=HtmlContent( 'and easy to do anywhere, even with Python')) + + def test_value_error_is_raised_on_to_emails_set_to_reply_to_list_of_strs(self): + from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) + self.maxDiff = None + to_emails = [ + ('test+to0@example.com', 'Example To Name 0'), + ('test+to1@example.com', 'Example To Name 1') + ] + + mail = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=to_emails, + 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')) + with self.assertRaises(ValueError): + mail.reply_to_list = ['test+reply_to0@example.com', 'test+reply_to1@example.com'] + + def test_value_error_is_raised_on_to_emails_set_to_reply_to_list_of_tuples(self): + from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) + self.maxDiff = None + to_emails = [ + ('test+to0@example.com', 'Example To Name 0'), + ('test+to1@example.com', 'Example To Name 1') + ] + + mail = Mail( + from_email=From('test+from@example.com', 'Example From Name'), + to_emails=to_emails, + 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')) + with self.assertRaises(ValueError): + mail.reply_to_list = [('test+reply_to@example.com', 'Test Name')] def test_error_is_not_raised_on_to_emails_set_to_list_of_tuples(self): from sendgrid.helpers.mail import (PlainTextContent, HtmlContent) diff --git a/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md b/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md new file mode 100644 index 000000000..55d6adf18 --- /dev/null +++ b/use_cases/send_a_single_email_with_multiple_reply_to_addresses.md @@ -0,0 +1,29 @@ +```python +import os +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail + +message = Mail( + from_email='from_email@example.com', + to_emails='to@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content='and easy to do anywhere, even with Python') +message.reply_to_list = [ + ReplyTo( + email='reply-to-1@example.com', + name="Reply To Name 1", + ), + ReplyTo( + email='reply-to-2@example.com', + name="Reply To Name 2", + ) +] +try: + sendgrid_client = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + response = sendgrid_client.send(message) + print(response.status_code) + print(response.body) + print(response.headers) +except Exception as e: + print(e) +``` From 9fe3e235d06475573b27fb521d31a592799cf8a6 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 22 Mar 2023 11:45:20 -0700 Subject: [PATCH 117/149] [Librarian] Version Bump --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d8f79db..802270cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Change Log All notable changes to this project will be documented in this file. +[2023-03-22] Version 6.10.0 +--------------------------- +**Library - Feature** +- [PR #1062](https://github.com/sendgrid/sendgrid-python/pull/1062): Add reply_to_list functionality. Thanks to [@thepuzzlemaster](https://github.com/thepuzzlemaster)! +- [PR #1059](https://github.com/sendgrid/sendgrid-python/pull/1059): Add Python 3.11 to the testing. Thanks to [@cclauss](https://github.com/cclauss)! + +**Library - Miscellaneous** +- [PR #1065](https://github.com/sendgrid/sendgrid-python/pull/1065): Create GitHub Action to lint Python code. Thanks to [@cclauss](https://github.com/cclauss)! +- [PR #1064](https://github.com/sendgrid/sendgrid-python/pull/1064): Upgrade GitHub Action test-and-deploy.yml. Thanks to [@cclauss](https://github.com/cclauss)! +- [PR #1063](https://github.com/sendgrid/sendgrid-python/pull/1063): Upgrade GitHub Action pr-lint.yml. Thanks to [@cclauss](https://github.com/cclauss)! + +**Library - Test** +- [PR #1058](https://github.com/sendgrid/sendgrid-python/pull/1058): Adding misc as PR type. Thanks to [@rakatyal](https://github.com/rakatyal)! + +**Library - Docs** +- [PR #1055](https://github.com/sendgrid/sendgrid-python/pull/1055): Modify README.md in alignment with SendGrid Support. Thanks to [@garethpaul](https://github.com/garethpaul)! +- [PR #1052](https://github.com/sendgrid/sendgrid-python/pull/1052): Fix link that has drifted. Thanks to [@jonathanberger](https://github.com/jonathanberger)! + + [2022-03-09] Version 6.9.7 -------------------------- **Library - Chore** From 5e9687caf9ed643362220e239b71537ed539c535 Mon Sep 17 00:00:00 2001 From: Twilio Date: Wed, 22 Mar 2023 11:45:21 -0700 Subject: [PATCH 118/149] Release 6.10.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 59b5dedc7..466501d04 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.9.7' +__version__ = '6.10.0' From 2fe145956a1ee50355f5da8deab401e1e118c736 Mon Sep 17 00:00:00 2001 From: Seth Ammons Date: Mon, 17 Apr 2023 04:48:43 -0400 Subject: [PATCH 119/149] fix: removing codedev test dependency (#1066) Fix build by removing dead dependency. Per the announcment: https://about.codecov.io/blog/message-regarding-the-pypi-package/ coddev deprecated their pypi package on april 12, 2023 causing the build to break We no longer are registered at codedev: https://app.codecov.io/gh/sendgrid/sendgrid-python "This repo has been deactivated" --- README.rst | 4 +--- test/requirements.txt | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 93062ba66..e23e2300e 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ -|Tests Badge| |codecov| |Python Versions| |PyPI Version| |Docker Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| +|Tests Badge| |Python Versions| |PyPI Version| |Docker Badge| |MIT licensed| |Twitter Follow| |GitHub contributors| |Open Source Helpers| **This library allows you to quickly and easily use the Twilio SendGrid Web API v3 via Python.** @@ -288,8 +288,6 @@ License :target: https://pypi.org/project/sendgrid/ .. |PyPI Version| image:: https://img.shields.io/pypi/v/sendgrid.svg :target: https://pypi.org/project/sendgrid/ -.. |codecov| image:: https://img.shields.io/codecov/c/github/sendgrid/sendgrid-python/main.svg?style=flat-square&label=Codecov+Coverage - :target: https://codecov.io/gh/sendgrid/sendgrid-python .. |Docker Badge| image:: https://img.shields.io/docker/automated/sendgrid/sendgrid-python.svg :target: https://hub.docker.com/r/sendgrid/sendgrid-python/ .. |MIT licensed| image:: https://img.shields.io/badge/license-MIT-blue.svg diff --git a/test/requirements.txt b/test/requirements.txt index 17302b22e..859c2456d 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -2,7 +2,6 @@ pyyaml Flask==1.1.4 six coverage -codecov mock itsdangerous==1.1.0 markupsafe==1.1.1 From bcb4f3379ec07f8d337d7aaa63d461259ec5ee41 Mon Sep 17 00:00:00 2001 From: manisha1997 Date: Tue, 14 Nov 2023 15:44:25 +0530 Subject: [PATCH 120/149] feat: geolocation setter in sendgrid-python for GDPR compliance --- .github/workflows/lint-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index 1c5f53f23..ba064864c 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -13,4 +13,4 @@ jobs: steps: - uses: actions/checkout@v3 - run: pip install --user ruff - - run: ruff --format=github --ignore="E722,F40,F841" --line-length=681 --target-version=py37 . + - run: ruff --format=github --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . From d3ddc8aa27707ec5e2b13a65dfa4c1e2bc837497 Mon Sep 17 00:00:00 2001 From: manisha1997 Date: Tue, 14 Nov 2023 15:46:52 +0530 Subject: [PATCH 121/149] revert changes --- .github/workflows/lint-python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index ba064864c..1c5f53f23 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -13,4 +13,4 @@ jobs: steps: - uses: actions/checkout@v3 - run: pip install --user ruff - - run: ruff --format=github --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . + - run: ruff --format=github --ignore="E722,F40,F841" --line-length=681 --target-version=py37 . From b5afcca6c2c4b29330f81f92b79bf2526645223a Mon Sep 17 00:00:00 2001 From: Manisha Singh Date: Fri, 24 Nov 2023 00:29:36 +0530 Subject: [PATCH 122/149] feat: geolocation setter in sendgrid-python for GDPR compliance (#1073) --- .github/workflows/lint-python.yml | 2 +- examples/dataresidency/set_region.py | 37 ++++++++++++++++++++++++++++ sendgrid/base_interface.py | 23 ++++++++++++++++- test/unit/test_sendgrid.py | 27 ++++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 examples/dataresidency/set_region.py create mode 100644 test/unit/test_sendgrid.py diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index 1c5f53f23..ace47f77d 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -13,4 +13,4 @@ jobs: steps: - uses: actions/checkout@v3 - run: pip install --user ruff - - run: ruff --format=github --ignore="E722,F40,F841" --line-length=681 --target-version=py37 . + - run: ruff --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . diff --git a/examples/dataresidency/set_region.py b/examples/dataresidency/set_region.py new file mode 100644 index 000000000..9aae2611f --- /dev/null +++ b/examples/dataresidency/set_region.py @@ -0,0 +1,37 @@ +import sendgrid +import os + +from sendgrid import Email, To, Content, Mail + +# Example 1 +# setting region to be "global" + +sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) +from_email = Email("example@abc.com") +to_email = To("example@abc.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/plain", "and easy to do anywhere, even with Python") +mail = Mail(from_email, to_email, subject, content) +sg.set_sendgrid_data_residency("global") +print(sg.client.host) +response = sg.client.mail.send.post(request_body=mail.get()) +print(response) +print(response.status_code) +print(response.body) +print(response.headers) + +# Example 2 +# setting region to "eu" +sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY_EU')) +sg.set_sendgrid_data_residency("eu") +from_email = Email("example@abc.com") +to_email = To("example@abc.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/plain", "and easy to do anywhere, even with Python") +print(sg.client.host) +mail = Mail(from_email, to_email, subject, content) +response = sg.client.mail.send.post(request_body=mail.get()) +print(response) +print(response.status_code) +print(response.body) +print(response.headers) \ No newline at end of file diff --git a/sendgrid/base_interface.py b/sendgrid/base_interface.py index 92b38247e..f94f09479 100644 --- a/sendgrid/base_interface.py +++ b/sendgrid/base_interface.py @@ -1,5 +1,6 @@ import python_http_client +region_host_dict = {'eu':'https://api.eu.sendgrid.com','global':'https://api.sendgrid.com'} class BaseInterface(object): def __init__(self, auth, host, impersonate_subuser): @@ -22,10 +23,10 @@ def __init__(self, auth, host, impersonate_subuser): """ from . import __version__ self.auth = auth - self.host = host self.impersonate_subuser = impersonate_subuser self.version = __version__ self.useragent = 'sendgrid/{};python'.format(self.version) + self.host = host self.client = python_http_client.Client( host=self.host, @@ -60,3 +61,23 @@ def send(self, message): message = message.get() return self.client.mail.send.post(request_body=message) + + def set_sendgrid_data_residency(self, region): + """ + Client libraries contain setters for specifying region/edge. + This supports global and eu regions only. This set will likely expand in the future. + Global is the default residency (or region) + Global region means the message will be sent through https://api.sendgrid.com + EU region means the message will be sent through https://api.eu.sendgrid.com + :param region: string + :return: + """ + if region in region_host_dict.keys(): + self.host = region_host_dict[region] + if self._default_headers is not None: + self.client = python_http_client.Client( + host=self.host, + request_headers=self._default_headers, + version=3) + else: + raise ValueError("region can only be \"eu\" or \"global\"") diff --git a/test/unit/test_sendgrid.py b/test/unit/test_sendgrid.py new file mode 100644 index 000000000..328d978ab --- /dev/null +++ b/test/unit/test_sendgrid.py @@ -0,0 +1,27 @@ +import unittest +import sendgrid + +class UnitTests(unittest.TestCase): + def test_host_with_no_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + self.assertEqual("https://api.sendgrid.com",sg.client.host) + + def test_host_with_eu_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + sg.set_sendgrid_data_residency("eu") + self.assertEqual("https://api.eu.sendgrid.com",sg.client.host) + + def test_host_with_global_region(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + sg.set_sendgrid_data_residency("global") + self.assertEqual("https://api.sendgrid.com",sg.client.host) + + def test_with_region_is_none(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + with self.assertRaises(ValueError): + sg.set_sendgrid_data_residency(None) + + def test_with_region_is_invalid(self): + sg = sendgrid.SendGridAPIClient(api_key='MY_API_KEY') + with self.assertRaises(ValueError): + sg.set_sendgrid_data_residency("abc") \ No newline at end of file From 4804d32b17e9d396e319c1943792537dc1846008 Mon Sep 17 00:00:00 2001 From: Twilio Date: Fri, 1 Dec 2023 05:16:21 +0000 Subject: [PATCH 123/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 802270cd8..4415a03f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2023-12-01] Version 6.11.0 +--------------------------- +**Library - Feature** +- [PR #1073](https://github.com/sendgrid/sendgrid-python/pull/1073): geolocation setter in sendgrid-python for GDPR compliance. Thanks to [@manisha1997](https://github.com/manisha1997)! + +**Library - Test** +- [PR #1066](https://github.com/sendgrid/sendgrid-python/pull/1066): removing codedev test dependency. Thanks to [@sethgrid](https://github.com/sethgrid)! + + [2023-03-22] Version 6.10.0 --------------------------- **Library - Feature** From 4e4ac47e61f52b9ead5020f807f4eb151f9a0bc6 Mon Sep 17 00:00:00 2001 From: Twilio Date: Fri, 1 Dec 2023 05:16:22 +0000 Subject: [PATCH 124/149] Release 6.11.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 466501d04..901082d42 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.10.0' +__version__ = '6.11.0' From 78acb96d25a299ad862396c1fbc399c897998bfd Mon Sep 17 00:00:00 2001 From: manisha1997 Date: Tue, 26 Dec 2023 13:53:33 +0530 Subject: [PATCH 125/149] chore: commit to main to check if the commits go directly into main. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 663d1e977..d41c0a1c4 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ response = sg.client.mail.send.post(request_body=data) print(response.status_code) print(response.body) print(response.headers) + ``` ## General v3 Web API Usage (With [Fluent Interface](https://sendgrid.com/blog/using-python-to-implement-a-fluent-interface-to-any-rest-api/)) From 9515dce6c8229c9cf7272701d0c2664c05727f16 Mon Sep 17 00:00:00 2001 From: manisha1997 Date: Tue, 26 Dec 2023 13:54:42 +0530 Subject: [PATCH 126/149] chore: rolling back direct commits --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d41c0a1c4..663d1e977 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ response = sg.client.mail.send.post(request_body=data) print(response.status_code) print(response.body) print(response.headers) - ``` ## General v3 Web API Usage (With [Fluent Interface](https://sendgrid.com/blog/using-python-to-implement-a-fluent-interface-to-any-rest-api/)) From e2a2cfd7c0632ead3ecdeb725ec4798b711a7931 Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 11 Apr 2025 15:00:51 +0530 Subject: [PATCH 127/149] chore: install docker-compose (#1099) * chore: install docker-compose * chore: update licence year * chore: fix lint-python --- .github/workflows/lint-python.yml | 2 +- .github/workflows/test-and-deploy.yml | 5 +++++ LICENSE | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-python.yml b/.github/workflows/lint-python.yml index ace47f77d..5dcf0328c 100644 --- a/.github/workflows/lint-python.yml +++ b/.github/workflows/lint-python.yml @@ -13,4 +13,4 @@ jobs: steps: - uses: actions/checkout@v3 - run: pip install --user ruff - - run: ruff --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . + - run: ruff check --ignore="E722,F40,F841" --line-length=320 --target-version=py37 . diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 4db532e3d..25408ac0a 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -31,6 +31,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_AUTH_TOKEN }} + - name: Install Docker Compose + run: | + sudo apt-get update + sudo apt-get install -y docker-compose + - name: Build & Test run: make test-docker version=${{ matrix.python-version }} diff --git a/LICENSE b/LICENSE index 3154774a9..126ceb1a3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (C) 2023, Twilio SendGrid, Inc. +Copyright (C) 2025, Twilio SendGrid, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From ee54d81051d45b0a667642866b74dc7efbc7155b Mon Sep 17 00:00:00 2001 From: Meysam Date: Tue, 15 Apr 2025 22:05:45 +0700 Subject: [PATCH 128/149] feat: add support for python3.12 and 3.13 (#1087) --- .github/workflows/test-and-deploy.yml | 2 +- Makefile | 6 ++--- setup.py | 2 ++ test/requirements.txt | 1 + tox.ini | 32 ++++++++++++++++++++++++++- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 25408ac0a..ba8b69905 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11' ] + python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' , '3.13'] env: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: diff --git a/Makefile b/Makefile index fb61004ce..96161106d 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,11 @@ venv: clean virtualenv --python=python venv install: venv + . venv/bin/activate; pip install -r test/requirements.txt . venv/bin/activate; python setup.py install . venv/bin/activate; pip install -r requirements.txt -test-install: install - . venv/bin/activate; pip install -r test/requirements.txt - -test: test-install +test: install . venv/bin/activate; coverage run -m unittest discover -s test/unit test-integ: test diff --git a/setup.py b/setup.py index 41f11e589..7c797ae67 100644 --- a/setup.py +++ b/setup.py @@ -41,5 +41,7 @@ def getRequires(): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] ) diff --git a/test/requirements.txt b/test/requirements.txt index 859c2456d..40552deba 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -5,3 +5,4 @@ coverage mock itsdangerous==1.1.0 markupsafe==1.1.1 +setuptools diff --git a/tox.ini b/tox.ini index 926a3c9dd..8f4f2db9a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36, py37 +envlist = py27, py34, py35, py36, py37, py38, py39, py310, py311, py312, py313 [testenv] commands = coverage erase @@ -39,3 +39,33 @@ basepython = python3.6 commands = {[testenv]commands} deps = {[testenv]deps} basepython = python3.7 + +[testenv:py38] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.8 + +[testenv:py39] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.9 + +[testenv:py310] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.10 + +[testenv:py311] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.11 + +[testenv:py312] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.12 + +[testenv:py313] +commands = {[testenv]commands} +deps = {[testenv]deps} +basepython = python3.13 From 09bc8466ddf1ecd11a6dc7ebb7a8338fb5afca86 Mon Sep 17 00:00:00 2001 From: GPK Date: Fri, 25 Apr 2025 14:06:33 +0100 Subject: [PATCH 129/149] chore: Add werkzeug package to setup file (#1098) * Add werkzeug package to setup file * Add werkzeug package to setup file * Update versions * Remove EOL python versions --------- Co-authored-by: Shubham --- .github/workflows/test-and-deploy.yml | 2 +- setup.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index ba8b69905..959970d94 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: [ '2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' , '3.13'] + python-version: [ '3.9', '3.10', '3.11', '3.12' , '3.13'] env: DOCKER_LOGIN: ${{ secrets.DOCKER_USERNAME && secrets.DOCKER_AUTH_TOKEN }} steps: diff --git a/setup.py b/setup.py index 7c797ae67..0753e3997 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,11 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'starkbank-ecdsa>=2.0.1' + 'starkbank-ecdsa>=2.0.1', + "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", + "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.6'", + "werkzeug>=2.0.0,<3.0.0 ; python_version >= '3.6' and python_version < '3.11'", + "werkzeug>=3.0.0 ; python_version >= '3.11'" ] return deps From ac693fe02295e5e0b77070bab6fa460c697f8997 Mon Sep 17 00:00:00 2001 From: Twilio Date: Mon, 5 May 2025 10:35:35 +0000 Subject: [PATCH 130/149] [Librarian] Version Bump --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4415a03f7..a2215795b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-05-05] Version 6.12.0 +--------------------------- +**Library - Chore** +- [PR #1098](https://github.com/sendgrid/sendgrid-python/pull/1098): Add werkzeug package to setup file. Thanks to [@gopidesupavan](https://github.com/gopidesupavan)! +- [PR #1099](https://github.com/sendgrid/sendgrid-python/pull/1099): install docker-compose. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + +**Library - Feature** +- [PR #1087](https://github.com/sendgrid/sendgrid-python/pull/1087): add support for python3.12 and 3.13. Thanks to [@meysam81](https://github.com/meysam81)! + + [2023-12-01] Version 6.11.0 --------------------------- **Library - Feature** From 774663f5a0af011d23908d0dbd932cb64ee96d70 Mon Sep 17 00:00:00 2001 From: Twilio Date: Mon, 5 May 2025 10:35:35 +0000 Subject: [PATCH 131/149] Release 6.12.0 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 901082d42..142f2122e 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.11.0' +__version__ = '6.12.0' From 6eb4375f4d1ef1e0c1ad17d07858706291380ebc Mon Sep 17 00:00:00 2001 From: ranjanprasad1996 <141220652+ranjanprasad1996@users.noreply.github.com> Date: Fri, 9 May 2025 20:54:52 +0530 Subject: [PATCH 132/149] fix: Vulnerability fix for starkbank-ecdsa 2.2.0 dependency (#1085) * Updated webhook helper to use a different edcsa library * chore: dummy commit * Update dependencies --------- Co-authored-by: Shubham --- requirements.txt | 6 ++--- sendgrid/helpers/eventwebhook/__init__.py | 30 +++++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0c34aafd4..c95204480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Flask==1.1.2 +Flask==3.1.0 PyYAML>=4.2b1 python-http-client>=3.2.1 -six==1.11.0 -starkbank-ecdsa>=2.0.1 +six==1.17.0 +ecdsa>=0.19.1,<1 more-itertools==5.0.0 diff --git a/sendgrid/helpers/eventwebhook/__init__.py b/sendgrid/helpers/eventwebhook/__init__.py index c1ec7d1c8..d97604df8 100644 --- a/sendgrid/helpers/eventwebhook/__init__.py +++ b/sendgrid/helpers/eventwebhook/__init__.py @@ -1,8 +1,7 @@ -from ellipticcurve.ecdsa import Ecdsa -from ellipticcurve.publicKey import PublicKey -from ellipticcurve.signature import Signature - -from .eventwebhook_header import EventWebhookHeader +from ecdsa import VerifyingKey, BadSignatureError +from ecdsa.util import sigdecode_der +import base64 +import hashlib class EventWebhook: """ @@ -20,14 +19,15 @@ def __init__(self, public_key=None): def convert_public_key_to_ecdsa(self, public_key): """ - Convert the public key string to a ECPublicKey. + Convert the public key string to a VerifyingKey object. :param public_key: verification key under Mail Settings :type public_key string - :return: public key using the ECDSA algorithm - :rtype PublicKey + :return: VerifyingKey object using the ECDSA algorithm + :rtype VerifyingKey """ - return PublicKey.fromPem('\n-----BEGIN PUBLIC KEY-----\n'+public_key+'\n-----END PUBLIC KEY-----\n') + pem_key = "-----BEGIN PUBLIC KEY-----\n" + public_key + "\n-----END PUBLIC KEY-----" + return VerifyingKey.from_pem(pem_key) def verify_signature(self, payload, signature, timestamp, public_key=None): """ @@ -40,11 +40,15 @@ def verify_signature(self, payload, signature, timestamp, public_key=None): :param timestamp: value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header :type timestamp: string :param public_key: elliptic curve public key - :type public_key: PublicKey + :type public_key: VerifyingKey :return: true or false if signature is valid """ - timestamped_payload = timestamp + payload - decoded_signature = Signature.fromBase64(signature) + timestamped_payload = (timestamp + payload).encode('utf-8') + decoded_signature = base64.b64decode(signature) key = public_key or self.public_key - return Ecdsa.verify(timestamped_payload, decoded_signature, key) + try: + key.verify(decoded_signature, timestamped_payload, hashfunc=hashlib.sha256, sigdecode=sigdecode_der) + return True + except BadSignatureError: + return False From 096719d8daf5846664eb3f6b4649c826176a959c Mon Sep 17 00:00:00 2001 From: Twilio Date: Tue, 13 May 2025 09:44:12 +0000 Subject: [PATCH 133/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2215795b..2936c9a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-05-13] Version 6.12.1 +--------------------------- +**Library - Fix** +- [PR #1085](https://github.com/sendgrid/sendgrid-python/pull/1085): Vulnerability fix for starkbank-ecdsa 2.2.0 dependency. Thanks to [@ranjanprasad1996](https://github.com/ranjanprasad1996)! + + [2025-05-05] Version 6.12.0 --------------------------- **Library - Chore** From 2acb010ae303cca78d696b82b739809b7f319187 Mon Sep 17 00:00:00 2001 From: Twilio Date: Tue, 13 May 2025 09:44:12 +0000 Subject: [PATCH 134/149] Release 6.12.1 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 142f2122e..431e91f2e 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.12.0' +__version__ = '6.12.1' From e1ed323bb0a595b7488fbb0c8d8351682ff97f55 Mon Sep 17 00:00:00 2001 From: Shubham Date: Tue, 13 May 2025 16:19:25 +0530 Subject: [PATCH 135/149] chore: update ecdsa in setup.py (#1102) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0753e3997..b3a8e5c86 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'starkbank-ecdsa>=2.0.1', + 'ecdsa>=0.19.1,<1', "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.6'", "werkzeug>=2.0.0,<3.0.0 ; python_version >= '3.6' and python_version < '3.11'", From a1acbf3a53e5f5b122614718acbdc6a7196921a9 Mon Sep 17 00:00:00 2001 From: Twilio Date: Tue, 13 May 2025 10:51:11 +0000 Subject: [PATCH 136/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2936c9a18..607b465c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-05-13] Version 6.12.2 +--------------------------- +**Library - Chore** +- [PR #1102](https://github.com/sendgrid/sendgrid-python/pull/1102): update ecdsa in setup.py. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + + [2025-05-13] Version 6.12.1 --------------------------- **Library - Fix** From 3f25a2a6ed33878e13cfe8d76d283b1d6c7b380a Mon Sep 17 00:00:00 2001 From: Twilio Date: Tue, 13 May 2025 10:51:12 +0000 Subject: [PATCH 137/149] Release 6.12.2 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 431e91f2e..67234a38e 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.12.1' +__version__ = '6.12.2' From a78bd11b854ab38978f73c7e3fda98110bd9f840 Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Wed, 14 May 2025 11:02:30 +0300 Subject: [PATCH 138/149] chore: Relax werkzeug version (#1103) --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b3a8e5c86..a1b1a9743 100644 --- a/setup.py +++ b/setup.py @@ -13,8 +13,11 @@ def getRequires(): 'ecdsa>=0.19.1,<1', "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.6'", - "werkzeug>=2.0.0,<3.0.0 ; python_version >= '3.6' and python_version < '3.11'", - "werkzeug>=3.0.0 ; python_version >= '3.11'" + "werkzeug>=0.15.0,<2.3.0 ; python_version >= '3.0' and python_version < '3.7'", + "werkzeug>=0.16.0,<3.1.0 ; python_version >= '3.0' and python_version < '3.8'", + "werkzeug>=1.0.0 ; python_version >= '3.9'", + "werkzeug>=2.2.0 ; python_version >= '3.11'", + "werkzeug>=2.3.5 ; python_version >= '3.12'" ] return deps From 4671192ce266f575eca28c062766ff4194d66ae9 Mon Sep 17 00:00:00 2001 From: Elad Kalif <45845474+eladkal@users.noreply.github.com> Date: Wed, 14 May 2025 17:35:46 +0300 Subject: [PATCH 139/149] chore: fix werkzeug lower versions (#1104) --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a1b1a9743..745edea81 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,9 @@ def getRequires(): 'python_http_client>=3.2.1', 'ecdsa>=0.19.1,<1', "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", - "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.6'", - "werkzeug>=0.15.0,<2.3.0 ; python_version >= '3.0' and python_version < '3.7'", - "werkzeug>=0.16.0,<3.1.0 ; python_version >= '3.0' and python_version < '3.8'", + "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.7'", + "werkzeug>=0.15.0,<2.3.0 ; python_version >= '3.0' and python_version < '3.8'", # version 2.3.0 dropped support for Python 3.7 + "werkzeug>=0.16.0,<3.1.0 ; python_version >= '3.0' and python_version < '3.9'", # version 3.1.0 dropped support for Python 3.8 "werkzeug>=1.0.0 ; python_version >= '3.9'", "werkzeug>=2.2.0 ; python_version >= '3.11'", "werkzeug>=2.3.5 ; python_version >= '3.12'" From 3e9d4efac5d652de81fb82ae38d256aad78c00d5 Mon Sep 17 00:00:00 2001 From: Shubham Date: Fri, 23 May 2025 11:51:29 +0530 Subject: [PATCH 140/149] chore: export EventWebhookHeader (#1107) --- sendgrid/helpers/eventwebhook/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sendgrid/helpers/eventwebhook/__init__.py b/sendgrid/helpers/eventwebhook/__init__.py index d97604df8..82a2cd6b8 100644 --- a/sendgrid/helpers/eventwebhook/__init__.py +++ b/sendgrid/helpers/eventwebhook/__init__.py @@ -2,6 +2,7 @@ from ecdsa.util import sigdecode_der import base64 import hashlib +from .eventwebhook_header import EventWebhookHeader class EventWebhook: """ From a24ab9de13b2b182985860e5979b1368d2e9fa95 Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 29 May 2025 12:11:32 +0000 Subject: [PATCH 141/149] [Librarian] Version Bump --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 607b465c5..7a69e3f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-05-29] Version 6.12.3 +--------------------------- +**Library - Chore** +- [PR #1107](https://github.com/sendgrid/sendgrid-python/pull/1107): export EventWebhookHeader. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! +- [PR #1104](https://github.com/sendgrid/sendgrid-python/pull/1104): fix werkzeug lower versions. Thanks to [@eladkal](https://github.com/eladkal)! +- [PR #1103](https://github.com/sendgrid/sendgrid-python/pull/1103): Relax werkzeug version. Thanks to [@eladkal](https://github.com/eladkal)! + + [2025-05-13] Version 6.12.2 --------------------------- **Library - Chore** From 159f5dd2d4a6eb360ca61356b009b37fcc6aa5c8 Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 29 May 2025 12:11:32 +0000 Subject: [PATCH 142/149] Release 6.12.3 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 67234a38e..67ae80f96 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.12.2' +__version__ = '6.12.3' From f5bf6153b5dabf6fe307a8acdb0e59ce17a0c07a Mon Sep 17 00:00:00 2001 From: Manisha Singh Date: Mon, 2 Jun 2025 16:40:55 +0530 Subject: [PATCH 143/149] chore: bug-fix (#1109) --- sendgrid/helpers/mail/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index 2472ad7d3..e475fe764 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -567,7 +567,7 @@ def add_custom_arg(self, custom_arg): :param value: A CustomArg object or a dict of custom arg key/values :type value: CustomArg, dict """ - if custom_arg.personalization is not None: + if not isinstance(custom_arg, dict) and custom_arg.personalization is not None: try: personalization = \ self._personalizations[custom_arg.personalization] From 59994bfafc59fbe7fa2bb9338654e5e6a4e4f612 Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 12 Jun 2025 10:27:17 +0000 Subject: [PATCH 144/149] [Librarian] Version Bump --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a69e3f92..87dde6650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-06-12] Version 6.12.4 +--------------------------- +**Library - Chore** +- [PR #1109](https://github.com/sendgrid/sendgrid-python/pull/1109): bug-fix. Thanks to [@manisha1997](https://github.com/manisha1997)! + + [2025-05-29] Version 6.12.3 --------------------------- **Library - Chore** From 7eafe1854f42ef2d6863a7288c1c2e73f9aa5c82 Mon Sep 17 00:00:00 2001 From: Twilio Date: Thu, 12 Jun 2025 10:27:18 +0000 Subject: [PATCH 145/149] Release 6.12.4 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 67ae80f96..127e2ed69 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.12.3' +__version__ = '6.12.4' From af2c4e70b338cdf4ccca4ed12184fdda5926dcc6 Mon Sep 17 00:00:00 2001 From: Shubham Date: Thu, 11 Sep 2025 13:20:25 +0530 Subject: [PATCH 146/149] chore: use make-test instead of make test-docker (#1117) --- .github/workflows/test-and-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 959970d94..98bbeefbd 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -37,7 +37,7 @@ jobs: sudo apt-get install -y docker-compose - name: Build & Test - run: make test-docker version=${{ matrix.python-version }} + run: make test deploy: name: Deploy From 68288529c5e510e6fe535c42d7e52fea5922558a Mon Sep 17 00:00:00 2001 From: David Acevedo Date: Thu, 11 Sep 2025 02:57:07 -0500 Subject: [PATCH 147/149] fix: #1108 - Replace ecdsa with cryptography (#1114) Co-authored-by: Shubham --- CONTRIBUTING.md | 2 +- README.md | 2 +- README.rst | 4 ++-- requirements.txt | 2 +- sendgrid/helpers/eventwebhook/__init__.py | 21 +++++++++++---------- setup.py | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 890641059..af9507154 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ We welcome direct contributions to the sendgrid-python code base. Thank you! - Python version 2.7, 3.5, 3.6, 3.7, or 3.8 - [python_http_client](https://github.com/sendgrid/python-http-client) -- [ecdsa_python](https://github.com/starkbank/ecdsa-python) +- [cryptography](https://github.com/pyca/cryptography) - [pyenv](https://github.com/yyuu/pyenv) - [tox](https://pypi.python.org/pypi/tox) diff --git a/README.md b/README.md index 663d1e977..b1b36686f 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ pip install sendgrid ## Dependencies - [Python-HTTP-Client](https://github.com/sendgrid/python-http-client) -- [ECDSA-Python](https://github.com/starkbank/ecdsa-python) +- [Cryptography](https://github.com/pyca/cryptography) diff --git a/README.rst b/README.rst index e23e2300e..526c4ca40 100644 --- a/README.rst +++ b/README.rst @@ -90,7 +90,7 @@ Dependencies ------------ - `Python-HTTP-Client`_ -- `ECDSA-Python`_ +- `Cryptography`_ Quick Start =========== @@ -259,7 +259,7 @@ License .. _Twilio account: https://www.twilio.com/try-twilio?source=sendgrid-python .. _SENDGRID_API_KEY: https://app.sendgrid.com/settings/api_keys .. _Python-HTTP-Client: https://github.com/sendgrid/python-http-client -.. _ECDSA-Python: https://github.com/starkbank/ecdsa-python +.. _Cryptography: https://github.com/pyca/cryptography .. _/mail/send Helper: https://github.com/sendgrid/sendgrid-python/tree/HEAD/sendgrid/helpers/mail .. _personalization object: https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/personalizations.html .. _Fluent Interface: https://sendgrid.com/blog/using-python-to-implement-a-fluent-interface-to-any-rest-api/ diff --git a/requirements.txt b/requirements.txt index c95204480..ed2594a90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ Flask==3.1.0 PyYAML>=4.2b1 python-http-client>=3.2.1 six==1.17.0 -ecdsa>=0.19.1,<1 +cryptography>=45.0.6 more-itertools==5.0.0 diff --git a/sendgrid/helpers/eventwebhook/__init__.py b/sendgrid/helpers/eventwebhook/__init__.py index 82a2cd6b8..9d618bf3a 100644 --- a/sendgrid/helpers/eventwebhook/__init__.py +++ b/sendgrid/helpers/eventwebhook/__init__.py @@ -1,7 +1,8 @@ -from ecdsa import VerifyingKey, BadSignatureError -from ecdsa.util import sigdecode_der +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import load_pem_public_key import base64 -import hashlib from .eventwebhook_header import EventWebhookHeader class EventWebhook: @@ -20,15 +21,15 @@ def __init__(self, public_key=None): def convert_public_key_to_ecdsa(self, public_key): """ - Convert the public key string to a VerifyingKey object. + Convert the public key string to an EllipticCurvePublicKey object. :param public_key: verification key under Mail Settings :type public_key string - :return: VerifyingKey object using the ECDSA algorithm - :rtype VerifyingKey + :return: An EllipticCurvePublicKey object using the ECDSA algorithm + :rtype cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey """ pem_key = "-----BEGIN PUBLIC KEY-----\n" + public_key + "\n-----END PUBLIC KEY-----" - return VerifyingKey.from_pem(pem_key) + return load_pem_public_key(pem_key.encode("utf-8")) def verify_signature(self, payload, signature, timestamp, public_key=None): """ @@ -41,7 +42,7 @@ def verify_signature(self, payload, signature, timestamp, public_key=None): :param timestamp: value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header :type timestamp: string :param public_key: elliptic curve public key - :type public_key: VerifyingKey + :type public_key: cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey :return: true or false if signature is valid """ timestamped_payload = (timestamp + payload).encode('utf-8') @@ -49,7 +50,7 @@ def verify_signature(self, payload, signature, timestamp, public_key=None): key = public_key or self.public_key try: - key.verify(decoded_signature, timestamped_payload, hashfunc=hashlib.sha256, sigdecode=sigdecode_der) + key.verify(decoded_signature, timestamped_payload, ec.ECDSA(hashes.SHA256())) return True - except BadSignatureError: + except InvalidSignature: return False diff --git a/setup.py b/setup.py index 745edea81..904cd654a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def getRequires(): deps = [ 'python_http_client>=3.2.1', - 'ecdsa>=0.19.1,<1', + 'cryptography>=45.0.6', "werkzeug>=0.11.15,<1.0.0 ; python_version < '3.0'", "werkzeug>=0.15.0,<2.0.0 ; python_version >= '3.0' and python_version < '3.7'", "werkzeug>=0.15.0,<2.3.0 ; python_version >= '3.0' and python_version < '3.8'", # version 2.3.0 dropped support for Python 3.7 From 5e26ead73a8d908550b4fab3166ab41697030c9c Mon Sep 17 00:00:00 2001 From: Shubham Tiwari Date: Fri, 19 Sep 2025 11:47:28 +0530 Subject: [PATCH 148/149] [Librarian] Version Bump --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87dde6650..67d327649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # Change Log All notable changes to this project will be documented in this file. +[2025-09-19] Version 6.12.5 +--------------------------- +**Library - Fix** +- [PR #1114](https://github.com/sendgrid/sendgrid-python/pull/1114): #1108 - Replace ecdsa with cryptography. Thanks to [@dacevedo12](https://github.com/dacevedo12)! + +**Library - Chore** +- [PR #1117](https://github.com/sendgrid/sendgrid-python/pull/1117): use make-test instead of make test-docker. Thanks to [@tiwarishubham635](https://github.com/tiwarishubham635)! + + [2025-06-12] Version 6.12.4 --------------------------- **Library - Chore** From 76788e70a76b11ce2990821f190e52f887ab9ed6 Mon Sep 17 00:00:00 2001 From: Shubham Tiwari Date: Fri, 19 Sep 2025 11:48:40 +0530 Subject: [PATCH 149/149] Release 6.12.5 --- sendgrid/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendgrid/version.py b/sendgrid/version.py index 127e2ed69..c1d623f90 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1 +1 @@ -__version__ = '6.12.4' +__version__ = '6.12.5'