diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index 7ad590b42..b3b7a4446 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -21,4 +21,4 @@ Closes #2 - - -If you have questions, please send an email to [Sendgrid](mailto:dx@sendgrid.com), or file a Github Issue in this repository. +If you have questions, please send an email to [SendGrid](mailto:dx@sendgrid.com), or file a GitHub Issue in this repository. diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f1479e4..7f59658f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ All notable changes to this project will be documented in this file. ## [5.4.1] - 2018-06-26 ## ### Fixed - [PR #585](https://github.com/sendgrid/sendgrid-python/pull/585): Fix typo in `mail_example.py`. Big thanks to [Anurag Anand](https://github.com/theanuraganand) for the PR! -- [PR #583](https://github.com/sendgrid/sendgrid-python/pull/585): Fix `Personalization.substitutions` setter. Trying to set substitutions directly rather than with add_substitution was causing an infinite regress. Big thanks to [Richard Nias](https://github.com/richardnias) for the PR! +- [PR #583](https://github.com/sendgrid/sendgrid-python/pull/583): Fix `Personalization.substitutions` setter. Trying to set substitutions directly rather than with add_substitution was causing an infinite regress. Big thanks to [Richard Nias](https://github.com/richardnias) for the PR! ## [5.4.0] - 2018-06-07 ## ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 39ed18bf7..2f1c6744f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -36,6 +36,6 @@ SendGrid thanks the following, on which it draws for content and inspiration: - [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/) - [Open Source Initiative General Code of Conduct](https://opensource.org/codeofconduct) - [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct.html) + * [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/) + * [Open Source Initiative General Code of Conduct](https://opensource.org/codeofconduct) + * [Apache Code of Conduct](https://www.apache.org/foundation/policies/conduct.html) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4aa2c4a5..b7d49ba80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ A software bug is a demonstrable issue in the code base. In order for us to diag 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. +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 issue has already been fixed 3. Copy and fill in the Bug Report Template we have provided below @@ -184,8 +184,10 @@ Please run your code through: ```bash # Clone your fork of the repo into the current directory git clone https://github.com/sendgrid/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 ``` @@ -233,4 +235,4 @@ If you have any additional questions, please feel free to [email](mailto:dx@send ## Code Reviews -If you can, please look at open PRs and review them. Give feedback and help us merge these PRs much faster! If you don't know how, Github has some great [information on how to review a Pull Request](https://help.github.com/articles/about-pull-request-reviews/). +If you can, please look at open PRs and review them. Give feedback and help us merge these PRs much faster! If you don't know how, GitHub has some great [information on how to review a Pull Request](https://help.github.com/articles/about-pull-request-reviews/). diff --git a/README.md b/README.md new file mode 100644 index 000000000..7072b4fd0 --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ + + +[](https://travis-ci.org/sendgrid/sendgrid-python) +[](https://codecov.io/gh/sendgrid/sendgrid-python) +[](https://hub.docker.com/r/sendgrid/sendgrid-python/) +[](https://dx.sendgrid.com/newsletter/python) +[](./LICENSE.txt) +[](https://twitter.com/sendgrid) +[](https://github.com/sendgrid/sendgrid-python/graphs/contributors) +[](https://www.codetriage.com/sendgrid/sendgrid-python) + +**NEW:** + +* Subscribe to email [notifications](https://dx.sendgrid.com/newsletter/python) for releases and breaking changes. +* Quickly get started with [Docker](https://github.com/sendgrid/sendgrid-python/tree/master/docker). + +**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). + +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](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md) or simply upvote or comment on existing issues or pull requests. + +Please browse the rest of this README for further detail. + +We appreciate your continued support, thank you! + +# Table of Contents + +* [Installation](#installation) +* [Quick Start](#quick-start) +* [Processing Inbound Email](#inbound) +* [Usage](#usage) +* [Use Cases](#use-cases) +* [Announcements](#announcements) +* [Roadmap](#roadmap) +* [How to Contribute](#contribute) +* [Troubleshooting](#troubleshooting) +* [About](#about) +* [License](#license) + + + +# Installation + +## Prerequisites + +- Python version 2.6, 2.7, 3.4, 3.5 or 3.6 +- The SendGrid service, starting at the [free level](https://sendgrid.com/free?source=sendgrid-python) + +## Setup Environment Variables +Update the development environment with your [SENDGRID_API_KEY](https://app.sendgrid.com/settings/api_keys), for example: +### Mac +```bash +echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env +echo "sendgrid.env" >> .gitignore +source ./sendgrid.env +``` +SendGrid also supports local environment file `.env`. Copy or rename `.env_sample` into `.env` and update [SENDGRID_API_KEY](https://app.sendgrid.com/settings/api_keys) with your key. + +### Windows +Temporarily set the environment variable(accessible only during the current cli session): +```bash +set SENDGRID_API_KEY=YOUR_API_KEY +``` +Permanently set the environment variable(accessible in all subsequent cli sessions): +```bash +setx SENDGRID_API_KEY "YOUR_API_KEY" +``` + +## Install Package +```bash +pip install sendgrid +``` + +## Dependencies + +- [Python-HTTP-Client](https://github.com/sendgrid/python-http-client) + + + +# Quick Start + +## Hello Email + +The following is the minimum needed code to send an email with the [/mail/send Helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail) ([here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/helpers/mail/mail_example.py#L20) is a full example): + +### With Mail Helper Class + +```python +import sendgrid +import os +from sendgrid.helpers.mail import * + +sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) +from_email = Email("test@example.com") +to_email = Email("test@example.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/plain", "and easy to do anywhere, even with Python") +mail = Mail(from_email, subject, to_email, content) +response = sg.client.mail.send.post(request_body=mail.get()) +print(response.status_code) +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](https://github.com/sendgrid/sendgrid-python/blob/master/examples/helpers/mail_example.py#L16) is an example of how to add it. + +### Without Mail Helper Class + +The following is the minimum needed code to send an email without the /mail/send Helper ([here](https://github.com/sendgrid/sendgrid-python/blob/master/examples/mail/mail.py#L27) is a full example): + +```python +import sendgrid +import os + +sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) +data = { + "personalizations": [ + { + "to": [ + { + "email": "test@example.com" + } + ], + "subject": "Sending with SendGrid is Fun" + } + ], + "from": { + "email": "test@example.com" + }, + "content": [ + { + "type": "text/plain", + "value": "and easy to do anywhere, even with Python" + } + ] +} +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/)) + +```python +import sendgrid +import os + +sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) +response = sg.client.suppression.bounces.get() +print(response.status_code) +print(response.body) +print(response.headers) +``` + +## General v3 Web API Usage (Without Fluent Interface) + +```python +import sendgrid +import os + +sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) +response = sg.client._("suppression/bounces").get() +print(response.status_code) +print(response.body) +print(response.headers) +``` + + +# Processing Inbound Email + +Please see [our helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound) for utilizing our Inbound Parse webhook. + + +# Usage + +- [SendGrid Documentation](https://sendgrid.com/docs/API_Reference/index.html) +- [Library Usage Documentation](https://github.com/sendgrid/sendgrid-python/tree/master/USAGE.md) +- [Example Code](https://github.com/sendgrid/sendgrid-python/tree/master/examples) +- [How-to: Migration from v2 to v3](https://sendgrid.com/docs/Classroom/Send/v3_Mail_Send/how_to_migrate_from_v2_to_v3_mail_send.html) +- [v3 Web API Mail Send Helper](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/mail) - build a request object payload for a v3 /mail/send API call. +- [Processing Inbound Email](https://github.com/sendgrid/sendgrid-python/tree/master/sendgrid/helpers/inbound) + + +# Use Cases + +[Examples of common API use cases](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md), such as how to send an email with a transactional template. + + +# Announcements + +Join an experienced and passionate team that focuses on making an impact. [Opportunities abound](https://sendgrid.com/careers) to grow the product - and grow your career! + +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](https://github.com/sendgrid/sendgrid-python/blob/master/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 + +We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md) guide for details. + +Quick links: + +- [Feature Request](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#feature-request) +- [Bug Reports](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#submit-a-bug-report) +- [Improvements to the Codebase](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#improvements-to-the-codebase) +- [Review Pull Requests](https://github.com/sendgrid/sendgrid-python/blob/master/CONTRIBUTING.md#code-reviews) +- [Sign the CLA to Create a Pull Request](https://cla.sendgrid.com/sendgrid/sendgrid-python) + + +# Troubleshooting + +Please see our [troubleshooting guide](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md) for common library issues. + + +# About + +sendgrid-python is guided and supported by the SendGrid Developer Experience Team. +Email the Developer Experience Team [here](mailto:dx@sendgrid.com) in case of any queries. + +sendgrid-python is maintained and funded by SendGrid, Inc. The names and logos for sendgrid-python are trademarks of SendGrid, Inc. + + +# License +[The MIT License (MIT)](LICENSE.txt) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 29c0c8e89..1298ab7d1 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -50,7 +50,7 @@ import urllib try: response = sg.client.mail.send.post(request_body=mail.get()) except urllib.error.HTTPError as e: - print e.read() + print(e.read()) ``` @@ -111,4 +111,4 @@ print mail.get() # Error Handling -Please review [our use_cases](https://github.com/sendgrid/sendgrid-python/use_cases/README.md) for examples of error handling. +Please review [our use_cases](https://github.com/sendgrid/sendgrid-python/blob/master/use_cases/README.md#use-cases) for examples of error handling. diff --git a/USE_CASES.md b/USE_CASES.md new file mode 100644 index 000000000..b59981e19 --- /dev/null +++ b/USE_CASES.md @@ -0,0 +1,322 @@ +This documentation provides examples for specific use cases. Please [open an issue](https://github.com/sendgrid/sendgrid-python/issues) or make a pull request for any use cases you would like us to document here. Thank you! + +# Table of Contents + +* [Transactional Templates](#transactional-templates) +* [Attachment](#attachment) +* [How to Setup a Domain Whitelabel](#domain_whitelabel) +* [How to View Email Statistics](#email_stats) +* [Asynchronous Mail Send](#asynchronous-mail-send) +* [Slack Event API Integration](#slack_event_integration) + + +# Transactional Templates + +For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html). Following is the template content we used for testing. + +Template ID (replace with your own): + +```text +13b8f94f-bcae-4ec6-b752-70d6cb59f932 +``` + +Email Subject: + +```text +<%subject%> +``` + +Template Body: + +```html + +
+Hello, {{name}}! Your current balance is {{balance}}
+ + """ + mail = Mail() + mail.from_email = Email('templates@sendgrid.com') + mail.template_id = 'd-your-dynamic-template-uid' + p = Personalization() + p.add_to(Email('user@example.com')) + p.dynamic_template_data = { + 'name': 'Bob', + 'balance': 42 + } + mail.add_personalization(p) + + sg = SendGridAPIClient() + response = sg.client.mail.send.post(request_body=mail.get()) + print(response.status_code) + print(response.headers) + print(response.body) diff --git a/examples/helpers/stats/stats_example.py b/examples/helpers/stats/stats_example.py new file mode 100644 index 000000000..d48664c3f --- /dev/null +++ b/examples/helpers/stats/stats_example.py @@ -0,0 +1,99 @@ +import json +import os +from sendgrid.helpers.stats import * +from sendgrid import * + +# NOTE: you will need move this file to the root directory of this project to execute properly. + +# Assumes you set your environment variable: +# https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key +sg = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + + +def pprint_json(json_raw): + print(json.dumps(json.loads(json_raw), indent=2, sort_keys=True)) + + +def build_global_stats(): + global_stats = Stats() + global_stats.start_date = '2017-10-14' + global_stats.end_date = '2017-10-20' + global_stats.aggregated_by = 'day' + return global_stats.get() + + +def build_category_stats(): + category_stats = CategoryStats('2017-10-15', ['foo', 'bar']) + # category_stats.start_date = '2017-10-15' + # category_stats.add_category(Category("foo")) + # category_stats.add_category(Category("bar")) + return category_stats.get() + + +def build_category_stats_sums(): + category_stats = CategoryStats() + category_stats.start_date = '2017-10-15' + category_stats.limit = 5 + category_stats.offset = 1 + return category_stats.get() + + +def build_subuser_stats(): + subuser_stats = SubuserStats('2017-10-20', ['aaronmakks','foo']) + # subuser_stats.start_date = '2017-10-15' + # subuser_stats.add_subuser(Subuser("foo")) + # subuser_stats.add_subuser(Subuser("bar")) + return subuser_stats.get() + +def build_subuser_stats_sums(): + subuser_stats = SubuserStats() + subuser_stats.start_date = '2017-10-15' + subuser_stats.limit = 5 + subuser_stats.offset = 1 + return subuser_stats.get() + + +def get_global_stats(): + stats_params = build_global_stats() + response = sg.client.stats.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + + +def get_category_stats(): + stats_params = build_category_stats() + response = sg.client.categories.stats.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + + +def get_category_stats_sums(): + stats_params = build_category_stats_sums() + response = sg.client.categories.stats.sums.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + + +def get_subuser_stats(): + stats_params = build_subuser_stats() + response = sg.client.subusers.stats.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + + +def get_subuser_stats_sums(): + stats_params = build_subuser_stats_sums() + response = sg.client.subusers.stats.sums.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + +get_global_stats() +get_category_stats() +get_category_stats_sums() +get_subuser_stats() +get_subuser_stats_sums() diff --git a/sendgrid/__init__.py b/sendgrid/__init__.py index 050e798e9..621b3b0d7 100644 --- a/sendgrid/__init__.py +++ b/sendgrid/__init__.py @@ -2,7 +2,7 @@ This library allows you to quickly and easily use the SendGrid Web API v3 via Python. -For more information on this library, see the README on Github. +For more information on this library, see the README on GitHub. https://github.com/sendgrid/sendgrid-python For more information on the SendGrid v3 API, see the v3 docs: http://sendgrid.com/docs/API_Reference/api_v3.html diff --git a/sendgrid/helpers/endpoints/__init__.py b/sendgrid/helpers/endpoints/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sendgrid/helpers/endpoints/ip/__init__.py b/sendgrid/helpers/endpoints/ip/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sendgrid/helpers/endpoints/ip/unassigned.py b/sendgrid/helpers/endpoints/ip/unassigned.py new file mode 100644 index 000000000..ff5edbd73 --- /dev/null +++ b/sendgrid/helpers/endpoints/ip/unassigned.py @@ -0,0 +1,52 @@ +import json + + +def format_ret(return_set, as_json=False): + """ decouple, allow for modifications to return type + returns a list of ip addresses in object or json form """ + ret_list = list() + for item in return_set: + d = {"ip": item} + ret_list.append(d) + + if as_json: + return json.dumps(ret_list) + + return ret_list + + +def unassigned(data, as_json=False): + """ https://sendgrid.com/docs/API_Reference/api_v3.html#ip-addresses + The /ips rest endpoint returns information about the IP addresses + and the usernames assigned to an IP + + unassigned returns a listing of the IP addresses that are allocated + but have 0 users assigned + + + data (response.body from sg.client.ips.get()) + as_json False -> get list of dicts + True -> get json object + + example: + sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + + params = {'subuser': 'test_string', 'ip': 'test_string', 'limit': 1, 'exclude_whitelabels': 'true', 'offset': 1} + response = sg.client.ips.get(query_params=params) + if response.status_code == 201: + data = response.body + unused = unassinged(data) """ + + no_subusers = set() + + if not isinstance(data, list): + return format_ret(no_subusers, as_json=as_json) + + for current in data: + num_subusers = len(current["subusers"]) + if num_subusers == 0: + current_ip = current["ip"] + no_subusers.add(current_ip) + + ret_val = format_ret(no_subusers, as_json=as_json) + return ret_val diff --git a/sendgrid/helpers/mail/mail.py b/sendgrid/helpers/mail/mail.py index f374e6ad1..7b586ee00 100644 --- a/sendgrid/helpers/mail/mail.py +++ b/sendgrid/helpers/mail/mail.py @@ -1,6 +1,7 @@ """v3/mail/send response body builder""" from .personalization import Personalization from .header import Header +from .email import Email class Mail(object): diff --git a/sendgrid/helpers/mail/personalization.py b/sendgrid/helpers/mail/personalization.py index 49a779cf9..125719009 100644 --- a/sendgrid/helpers/mail/personalization.py +++ b/sendgrid/helpers/mail/personalization.py @@ -1,6 +1,10 @@ class Personalization(object): """A Personalization defines who should receive an individual message and how that message should be handled. + + :var dynamic_template_data: data for dynamic transactional template. + Should be JSON-serializeable structure. No pre-processing will be done + prior to sending this via http client. """ def __init__(self): @@ -13,6 +17,7 @@ def __init__(self): self._substitutions = [] self._custom_args = [] self._send_at = None + self._dynamic_template_data = None @property def tos(self): @@ -158,6 +163,18 @@ def send_at(self): def send_at(self, value): self._send_at = value + @property + def dynamic_template_data(self): + """Data for dynamic transactional template. + + :rtype: JSON-serializeable structure + """ + return self._dynamic_template_data + + @dynamic_template_data.setter + def dynamic_template_data(self, json): + self._dynamic_template_data = json + def get(self): """ Get a JSON-ready representation of this Personalization. diff --git a/sendgrid/helpers/stats/README.md b/sendgrid/helpers/stats/README.md new file mode 100644 index 000000000..1fe31558b --- /dev/null +++ b/sendgrid/helpers/stats/README.md @@ -0,0 +1,10 @@ +**This helper allows you to quickly and easily build a Stats object for sending your email stats to a database.** + +# Quick Start + +Run the [example](https://github.com/sendgrid/sendgrid-python/tree/master/examples/helpers/stats) (make sure you have set your environment variable to include your SENDGRID_API_KEY). + +## Usage + +- See the [examples](https://github.com/sendgrid/sendgrid-python/tree/master/examples/helpers/stats) for complete working examples. +- [Documentation](https://sendgrid.com/docs/API_Reference/Web_API_v3/Stats/index.html) \ No newline at end of file diff --git a/sendgrid/helpers/stats/__init__.py b/sendgrid/helpers/stats/__init__.py new file mode 100644 index 000000000..9ee4dcdd8 --- /dev/null +++ b/sendgrid/helpers/stats/__init__.py @@ -0,0 +1 @@ +from .stats import * # noqa diff --git a/sendgrid/helpers/stats/stats.py b/sendgrid/helpers/stats/stats.py new file mode 100644 index 000000000..8fe1399a2 --- /dev/null +++ b/sendgrid/helpers/stats/stats.py @@ -0,0 +1,222 @@ +class Stats(object): + def __init__( + self, start_date=None): + self._start_date = None + self._end_date = None + self._aggregated_by = None + self._sort_by_metric = None + self._sort_by_direction = None + self._limit = None + self._offset = None + + # Minimum required for stats + if start_date: + self.start_date = start_date + + def __str__(self): + return str(self.get()) + + def get(self): + """ + :return: response stats dict + """ + stats = {} + if self.start_date is not None: + stats["start_date"] = self.start_date + if self.end_date is not None: + stats["end_date"] = self.end_date + if self.aggregated_by is not None: + stats["aggregated_by"] = self.aggregated_by + if self.sort_by_metric is not None: + stats["sort_by_metric"] = self.sort_by_metric + if self.sort_by_direction is not None: + stats["sort_by_direction"] = self.sort_by_direction + if self.limit is not None: + stats["limit"] = self.limit + if self.offset is not None: + stats["offset"] = self.offset + return stats + + @property + def start_date(self): + return self._start_date + + @start_date.setter + def start_date(self, value): + self._start_date = value + + @property + def end_date(self): + return self._end_date + + @end_date.setter + def end_date(self, value): + self._end_date = value + + @property + def aggregated_by(self): + return self._aggregated_by + + @aggregated_by.setter + def aggregated_by(self, value): + self._aggregated_by = value + + @property + def sort_by_metric(self): + return self._sort_by_metric + + @sort_by_metric.setter + def sort_by_metric(self, value): + self._sort_by_metric = value + + @property + def sort_by_direction(self): + return self._sort_by_direction + + @sort_by_direction.setter + def sort_by_direction(self, value): + self._sort_by_direction = value + + @property + def limit(self): + return self._limit + + @limit.setter + def limit(self, value): + self._limit = value + + @property + def offset(self): + return self._offset + + @offset.setter + def offset(self, value): + self._offset = value + + +class CategoryStats(Stats): + def __init__(self, start_date=None, categories=None): + self._categories = None + super(CategoryStats, self).__init__() + + # Minimum required for category stats + if start_date and categories: + self.start_date = start_date + for cat_name in categories: + self.add_category(Category(cat_name)) + + def get(self): + """ + :return: response stats dict + """ + stats = {} + if self.start_date is not None: + stats["start_date"] = self.start_date + if self.end_date is not None: + stats["end_date"] = self.end_date + if self.aggregated_by is not None: + stats["aggregated_by"] = self.aggregated_by + if self.sort_by_metric is not None: + stats["sort_by_metric"] = self.sort_by_metric + if self.sort_by_direction is not None: + stats["sort_by_direction"] = self.sort_by_direction + if self.limit is not None: + stats["limit"] = self.limit + if self.offset is not None: + stats["offset"] = self.offset + if self.categories is not None: + stats['categories'] = [category.get() for category in + self.categories] + return stats + + @property + def categories(self): + return self._categories + + def add_category(self, category): + if self._categories is None: + self._categories = [] + self._categories.append(category) + + +class SubuserStats(Stats): + def __init__(self, start_date=None, subusers=None): + self._subusers = None + super(SubuserStats, self).__init__() + + # Minimum required for subusers stats + if start_date and subusers: + self.start_date = start_date + for subuser_name in subusers: + self.add_subuser(Subuser(subuser_name)) + + def get(self): + """ + :return: response stats dict + """ + stats = {} + if self.start_date is not None: + stats["start_date"] = self.start_date + if self.end_date is not None: + stats["end_date"] = self.end_date + if self.aggregated_by is not None: + stats["aggregated_by"] = self.aggregated_by + if self.sort_by_metric is not None: + stats["sort_by_metric"] = self.sort_by_metric + if self.sort_by_direction is not None: + stats["sort_by_direction"] = self.sort_by_direction + if self.limit is not None: + stats["limit"] = self.limit + if self.offset is not None: + stats["offset"] = self.offset + if self.subusers is not None: + stats['subusers'] = [subuser.get() for subuser in + self.subusers] + return stats + + @property + def subusers(self): + return self._subusers + + def add_subuser(self, subuser): + if self._subusers is None: + self._subusers = [] + self._subusers.append(subuser) + + +class Category(object): + + def __init__(self, name=None): + self._name = None + if name is not None: + self._name = name + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + def get(self): + return self.name + + +class Subuser(object): + + def __init__(self, name=None): + self._name = None + if name is not None: + self._name = name + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + def get(self): + return self.name diff --git a/sendgrid/sendgrid.py b/sendgrid/sendgrid.py index b1665cf61..9a887a845 100644 --- a/sendgrid/sendgrid.py +++ b/sendgrid/sendgrid.py @@ -2,7 +2,7 @@ This library allows you to quickly and easily use the SendGrid Web API v3 via Python. -For more information on this library, see the README on Github. +For more information on this library, see the README on GitHub. https://github.com/sendgrid/sendgrid-python For more information on the SendGrid v3 API, see the v3 docs: http://sendgrid.com/docs/API_Reference/api_v3.html diff --git a/sendgrid/version.py b/sendgrid/version.py new file mode 100644 index 000000000..fffe2c469 --- /dev/null +++ b/sendgrid/version.py @@ -0,0 +1,2 @@ +version_info = (5, 6, 0) +__version__ = '.'.join(str(v) for v in version_info) diff --git a/test/test_mail.py b/test/test_mail.py index 98af29a09..0c0b668a2 100644 --- a/test/test_mail.py +++ b/test/test_mail.py @@ -557,3 +557,26 @@ def test_disable_tracking(self): def test_directly_setting_substitutions(self): personalization = Personalization() personalization.substitutions = [{'a': 0}] + + def test_dynamic_template_data(self): + p = Personalization() + p.add_to(Email('test@sendgrid.com')) + p.dynamic_template_data = { + 'customer': { + 'name': 'Bob', + 'returning': True + }, + 'total': 42 + } + + expected = { + 'to': [{'email': 'test@sendgrid.com'}], + 'dynamic_template_data': { + 'customer': { + 'name': 'Bob', + 'returning': True + }, + 'total': 42 + } + } + self.assertDictEqual(p.get(), expected) diff --git a/test/test_stats.py b/test/test_stats.py new file mode 100644 index 000000000..c71117397 --- /dev/null +++ b/test/test_stats.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +import json +from sendgrid.helpers.stats import * + +try: + import unittest2 as unittest +except ImportError: + import unittest + + +class UnitTests(unittest.TestCase): + + def test_basicStats(self): + + """Minimum required for stats""" + global_stats = Stats(start_date='12-09-2017') + + self.assertEqual( + json.dumps( + global_stats.get(), + sort_keys=True), + '{"start_date": "12-09-2017"}' + ) + + self.assertTrue(isinstance(str(global_stats), str)) + + def test_Stats(self): + + all_stats = Stats(start_date='12-09-2017') + all_stats.end_date = '12-10-2017' + all_stats.aggregated_by = 'day' + all_stats._sort_by_direction = 'asc' + all_stats.sort_by_metric = 'clicks' + all_stats._limit = 100 + all_stats._offset = 2 + + self.assertEqual( + json.dumps( + all_stats.get(), + sort_keys=True), + '{"aggregated_by": "day", "end_date": "12-10-2017", ' + '"limit": 100, "offset": 2, "sort_by_direction": "asc", ' + '"sort_by_metric": "clicks", "start_date": "12-09-2017"}' + ) + + def test_categoryStats(self): + + category_stats = CategoryStats(start_date='12-09-2017', categories=['foo', 'bar']) + category_stats.add_category(Category('woo')) + category_stats.end_date = '12-10-2017' + category_stats.aggregated_by = 'day' + category_stats._sort_by_direction = 'asc' + category_stats.sort_by_metric = 'clicks' + category_stats._limit = 100 + category_stats._offset = 2 + + self.assertEqual( + json.dumps( + category_stats.get(), + sort_keys=True), + '{"aggregated_by": "day", "categories": ["foo", "bar", "woo"], ' + '"end_date": "12-10-2017", "limit": 100, "offset": 2, ' + '"sort_by_direction": "asc", "sort_by_metric": "clicks", ' + '"start_date": "12-09-2017"}' + ) + + def test_subuserStats(self): + + subuser_stats = SubuserStats(start_date = '12-09-2017', subusers=['foo', 'bar']) + subuser_stats.add_subuser(Subuser('blah')) + subuser_stats.end_date = '12-10-2017' + subuser_stats.aggregated_by = 'day' + subuser_stats._sort_by_direction = 'asc' + subuser_stats.sort_by_metric = 'clicks' + subuser_stats._limit = 100 + subuser_stats._offset = 2 + + self.assertEqual( + json.dumps( + subuser_stats.get(), + sort_keys=True), + '{"aggregated_by": "day", "end_date": "12-10-2017", ' + '"limit": 100, "offset": 2, "sort_by_direction": "asc", ' + '"sort_by_metric": "clicks", "start_date": "12-09-2017", ' + '"subusers": ["foo", "bar", "blah"]}' + ) diff --git a/test/test_unassigned.py b/test/test_unassigned.py new file mode 100644 index 000000000..d13451277 --- /dev/null +++ b/test/test_unassigned.py @@ -0,0 +1,95 @@ +import json +import pytest + +from sendgrid.helpers.endpoints.ip.unassigned import unassigned + + +ret_json = '''[ { + "ip": "167.89.21.3", + "pools": [ + "pool1", + "pool2" + ], + "whitelabeled": false, + "start_date": 1409616000, + "subusers": [ + "tim@sendgrid.net" + ], + "warmup": false, + "assigned_at": 1482883200 + }, + { + "ip": "192.168.1.1", + "pools": [ + "pool1", + "pool2" + ], + "whitelabeled": false, + "start_date": 1409616000, + "subusers": [ + "tim@sendgrid.net" + ], + "warmup": false, + "assigned_at": 1482883200 + }, + { + "ip": "208.115.214.22", + "pools": [], + "whitelabeled": true, + "rdns": "o1.email.burgermail.com", + "start_date": 1409616000, + "subusers": [], + "warmup": false, + "assigned_at": 1482883200 + }, + { + "ip": "208.115.214.23", + "pools": [], + "whitelabeled": true, + "rdns": "o1.email.burgermail.com", + "start_date": 1409616000, + "subusers": [], + "warmup": false, + "assigned_at": 1482883200 + + } ] + ''' + +def get_all_ip(): + ret_val = json.loads(ret_json) + return ret_val + + +def make_data(): + data = set() + data.add("208.115.214.23") + data.add("208.115.214.22") + return data + + + +def test_unassigned_ip_json(): + + data = make_data() + + as_json = True + calculated = unassigned(get_all_ip(), as_json=as_json) + calculated = json.loads(calculated) + + for item in calculated: + assert item["ip"] in data + +def test_unassigned_ip_obj(): + + data = make_data() + + as_json = False + calculated = unassigned(get_all_ip(), as_json=as_json) + + for item in calculated: + assert item["ip"] in data + +def test_unassigned_baddata(): + as_json = False + calculated = unassigned(dict(), as_json=as_json) + assert calculated == [] diff --git a/use_cases/README.md b/use_cases/README.md index 188464d09..9966616e5 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 * [How to Create a Django app, Deployed on Heroku, to Send Email with SendGrid](django.md) * [How to Deploy A Simple Hello Email App on AWS](aws.md) +* [How to Deploy a simple Flask app, to send Email with SendGrid, on Heroku](flask_heroku.md) * [How to Setup a Domain Whitelabel](domain_whitelabel.md) * [How to View Email Statistics](email_stats.md) @@ -15,6 +16,7 @@ This directory provides examples for specific use cases of this library. Please * [Asynchronous Mail Send](asynchronous_mail_send.md) * [Attachment](attachment.md) * [Transactional Templates](transational_templates.md) +* [Integrate with Slack Events API](slack_event_api_integration.md) ### Library Features * [Error Handling](error_handling.md) \ No newline at end of file diff --git a/use_cases/aws.md b/use_cases/aws.md index 9c30fd7ed..13a4bbfe3 100644 --- a/use_cases/aws.md +++ b/use_cases/aws.md @@ -13,7 +13,7 @@ Python 2.7 and 3.4 or 3.5 are supported by the sendgrid Python library, however Before starting this tutorial, you will need to have access to an AWS account in which you are allowed to provision resources. This tutorial also assumes you've already created a SendGrid account with free-tier access. Finally, it is highly recommended you utilize [virtualenv](https://virtualenv.pypa.io/en/stable/). -*DISCLAIMER*: Any resources provisioned here may result in charges being incurred to your account. Sendgrid is in no way responsible for any billing charges. +*DISCLAIMER*: Any resources provisioned here may result in charges being incurred to your account. SendGrid is in no way responsible for any billing charges. ## Getting Started @@ -36,15 +36,15 @@ On the next menu, you have the option to choose what programming language you'll Follow the steps on the next screen. Choose a name for your API key, such as "hello-email". Follow the remaining steps to create an environment variable, install the sendgrid module, and copy the test code. Once that is complete, check the "I've integrated the code above" box, and click the "Next: Verify Integration" button. -Assuming all the steps were completed correctly, you should be greeted with a success message. If not, go back and verify that everything is correct, including your API key environment varible, and Python code. +Assuming all the steps were completed correctly, you should be greeted with a success message. If not, go back and verify that everything is correct, including your API key environment variable, and Python code. ## Deploy hello-world app using CodeStar -For the rest of the tutorial, we'll be working out of the git repository we cloned from AWS earlier: +For the rest of the tutorial, we'll be working out of the Git repository we cloned from AWS earlier: ``` $ cd hello-email ``` -note: this assumes you cloned the git repo inside your current directory. My directory is: +note: this assumes you cloned the Git repo inside your current directory. My directory is: ``` ~/projects/hello-email @@ -139,7 +139,7 @@ def handler(event, context): 'headers': {'Content-Type': 'application/json'}} ``` -Note that for the most part, we've simply copied the intial code from the API verification with SendGrid. Some slight modifications were needed to allow it to run as a lambda function, and for the output to be passed cleanly from the API endpoint. +Note that for the most part, we've simply copied the initial code from the API verification with SendGrid. Some slight modifications were needed to allow it to run as a lambda function, and for the output to be passed cleanly from the API endpoint. Change the `test@example.com` emails appropriately so that you may receive the test email. diff --git a/use_cases/flask_heroku.md b/use_cases/flask_heroku.md new file mode 100644 index 000000000..ef0fa599a --- /dev/null +++ b/use_cases/flask_heroku.md @@ -0,0 +1,9 @@ +# Create a Flask app to send email with SendGrid + +This tutorial explains how to deploy a simple Flask app, to send an email using the SendGrid Python SDK, on Heroku. + +1. Create a SendGrid API key at https://app.sendgrid.com/settings/api_keys +1. Go to https://github.com/swapagarwal/sendgrid-flask-heroku +1. Click on `Deploy to Heroku` button, and follow the instructions. + +That's all. You'll be sending your first email within seconds! diff --git a/use_cases/slack_event_api_integration.md b/use_cases/slack_event_api_integration.md new file mode 100644 index 000000000..ecce40695 --- /dev/null +++ b/use_cases/slack_event_api_integration.md @@ -0,0 +1,46 @@ +# Integrate with Slack Events API + +It's fairly straightforward to integrate SendGrid with Slack, to allow emails to be triggered by events happening on Slack. + +For this, we make use of the [Official Slack Events API](https://github.com/slackapi/python-slack-events-api), which can be installed using pip. + +To allow our application to get notifications of slack events, we first create a Slack App with Event Subscriptions as described [here](https://github.com/slackapi/python-slack-events-api#--development-workflow) + +Then, we set `SENDGRID_API_KEY` _(which you can create on the SendGrid dashboard)_ and `SLACK_VERIFICATION_TOKEN` _(which you can get in the App Credentials section of the Slack App)_ as environment variables. + +Once this is done, we can subscribe to [events on Slack](https://api.slack.com/events) and trigger emails when an event occurs. In the example below, we trigger an email to `test@example.com` whenever someone posts a message on Slack that has the word "_help_" in it. + +``` +from slackeventsapi import SlackEventAdapter +from slackclient import SlackClient +import os +import sendgrid +from sendgrid.helpers.mail import * + +sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + +SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"] +slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, "/slack/events") + +@slack_events_adapter.on("message") +def handle_message(event_data): + message = event_data["event"] + # If the incoming message contains "help", then send an email using SendGrid + if message.get("subtype") is None and "help" in message.get('text').lower(): + message = "Someone needs your help: \n\n %s" % message["text"] + r = send_email(message) + print(r) + + +def send_email(message): + from_email = Email("slack_integration@example.com") + to_email = Email("test@example.com") + subject = "Psst... Someone needs help!" + content = Content("text/plain", message) + mail = Mail(from_email, subject, to_email, content) + response = sg.client.mail.send.post(request_body=mail.get()) + return response.status_code + +# Start the slack event listener server on port 3000 +slack_events_adapter.start(port=3000) +``` \ No newline at end of file diff --git a/use_cases/transational_templates.md b/use_cases/transational_templates.md index d3e3a005d..491d528bd 100644 --- a/use_cases/transational_templates.md +++ b/use_cases/transational_templates.md @@ -1,6 +1,71 @@ -# Transactional Templates +### Transactional Templates -For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/index.html). Following is the template content we used for testing. +SendGrid transactional templates let you leverage power of [handlebars](https://handlebarsjs.com/) +syntax to easily manage complex dynamic content in transactional emails. + +For this example, we assume you have created a [transactional template](https://sendgrid.com/docs/User_Guide/Transactional_Templates/create_and_edit_transactional_templates.html). Following is the template content we used for testing. + +This example also assumes you [set your environment variable](https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key) with your SendGrid API Key. + +Template ID (replace with your own): + +```text +d-13b8f94fbcae4ec6b75270d6cb59f932 +``` + +Email Subject: + +```text +{{ subject }} +``` + +Template Body: + +```html + +
+