diff --git a/.env_sample b/.env_sample new file mode 100644 index 000000000..4337b4d53 --- /dev/null +++ b/.env_sample @@ -0,0 +1,3 @@ +SENDGRID_API_KEY=your_sendgrid_api_key +SENDGRID_USERNAME=your_sendgrid_username +SENDGRID_PASSWORD=your_sendgrid_password \ No newline at end of file diff --git a/.gitignore b/.gitignore index 31704303d..c5403fbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,8 @@ sdist *.egg *.egg-info *.pyc -.idea/ venv/ +.idea +.env +.python-version +.tox/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 68fc78387..b9eab6cde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ notifications: Build %{build_number} on branch %{branch} by %{author}: %{message} View on GitHub' format: html - notify: true + notify: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ec0feca..aac011323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # Change Log All notable changes to this project will be documented in this file. -## [1.4.3] - 2015-10-22 +## [1.5.3] - 2015-09-29 +### Added +- Refactored tests and added Tox support +- Framework for Web API v3 endpoints +- Web API v3 endpionts: apikeys, ASM groups and ASM suppressions + +### Fixed +- Python 3 Fix [#126](https://github.com/sendgrid/sendgrid-python/issues/126) + +## [1.4.3] - 2015-09-22 ### Fixed - Reply To header now supports friendly name [#110](https://github.com/sendgrid/sendgrid-python/issues/110) diff --git a/README.rst b/README.rst index f70d7f233..7a0a55d26 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Announcements For users of our `Web API v3 endpoints`_, we have begun integrating v3 endpoints into this library. As part of this process we have implemented a test automation tool, TOX_. We are also updating and enhancing the core library code. -In no particular order, we have implemented a few of the v3 endpoints already and would appreciate your feedback. Please feel free to submit issues and pull requests on the `v3_beta branch`_. +In no particular order, we have implemented a `few of the v3`_ endpoints already and would appreciate your feedback. Thank you for your continued support! @@ -232,6 +232,76 @@ add_content_id message.add_attachment('image.png', open('./image.png', 'rb')) message.add_content_id('image.png', 'ID_IN_HTML') message.set_html('TEXT BEFORE IMAGEAFTER IMAGE') + +WEB API v3 +---------- + +.. _APIKeysAnchor: + +`APIKeys`_ +~~~~~~~~~~ + +List all API Keys belonging to the authenticated user. + +.. code:: python + + client = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + status, msg = client.apikeys.get() + +`Advanced Suppression Manager (ASM)`_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Advanced Suppression Manager gives your recipients more control over the types of emails they want to receive by letting them opt out of messages from a certain type of email. + +More information_. + +.. _information: https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/index.html + +ASM Groups +~~~~~~~~~~ + +Retrieve all suppression groups associated with the user. + +.. code:: python + + client = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + status, msg = client.asm_groups.get() + +Get a single record. + +.. code:: python + + status, msg = client.asm_groups.get(record_id) + +ASM Suppressions +~~~~~~~~~~~~~~~~ + +Suppressions are email addresses that can be added to groups to prevent certain types of emails from being delivered to those addresses. + +Add recipient addresses to the suppressions list for a given group. + +.. code:: python + + client = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + group_id = # If no group_id_number, the emails will be added to the global suppression group + emails = ['elmer+test@thinkingserious.com', 'elmer+test2@thinkingserious.com'] + status, msg = client.asm_suppressions.post(group_id, emails) + +Get suppressed addresses for a given group. + +.. code:: python + + status, msg = client.asm_suppressions.get() + +Get suppression groups associated with a given recipient address. + +.. code:: python + + status, msg = client.asm_suppressions.get(None,) + +Delete a recipient email from the suppressions list for a group. + + status, msg = client.asm_suppressions.delete(,) SendGrid's `X-SMTPAPI`_ ----------------------- @@ -380,7 +450,7 @@ set_asm_group_id Using Templates from the Template Engine ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: python + message.add_filter('templates', 'enable', '1') message.add_filter('templates', 'template_id', 'TEMPLATE-ALPHA-NUMERIC-ID') @@ -388,12 +458,40 @@ Using Templates from the Template Engine Tests ~~~~~ +**Prerequisites:** + +- Mac OS X Prerequisite: + +.. code:: python + + xcode-select --install + +- Install pyenv and tox + +.. code:: python + + brew update + brew install pyenv + pip install tox + +- Add `eval "$(pyenv init -)"` to your profile after installing tox, you only need to do this once. + +.. code:: python + + pyenv install 2.6.9 + pyenv install 2.7.8 + pyenv install 3.2.6 + +**Run the tests:** + .. code:: python virtualenv venv - source venv/bin/activate + source venv/bin/activate #or . ./activate.sh python setup.py install - python test/__init__.py + pyenv local 3.2.6 2.7.8 2.6.9 + pyenv rehash + tox Deploying ~~~~~~~~~ @@ -419,4 +517,4 @@ MIT License .. _Filter: http://sendgrid.com/docs/API_Reference/SMTP_API/apps.html .. _`Web API v3 endpoints`: https://sendgrid.com/docs/API_Reference/Web_API_v3/index.html .. _TOX: https://testrun.org/tox/latest/ -.. _`v3_beta branch`: https://github.com/sendgrid/sendgrid-python/tree/v3_beta +.. _`few of the v3`: APIKeysAnchor_ diff --git a/activate.sh b/activate.sh new file mode 100755 index 000000000..0fe1b6dc6 --- /dev/null +++ b/activate.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Use this to activate the virtual environment, use the following to execute in current shell +# . ./activate +source venv/bin/activate \ No newline at end of file diff --git a/example_v2_test.py b/example_v2_test.py new file mode 100755 index 000000000..334411e54 --- /dev/null +++ b/example_v2_test.py @@ -0,0 +1,19 @@ +import sendgrid +import os +if os.path.exists('.env'): + for line in open('.env'): + var = line.strip().split('=') + if len(var) == 2: + os.environ[var[0]] = var[1] + +sg = sendgrid.SendGridClient(os.environ.get('SENDGRID_USERNAME'), os.environ.get('SENDGRID_PASSWORD')) + +message = sendgrid.Mail() +message.add_to('Elmer Thomas ') +message.set_subject('Testing from the Python library') +message.set_html('This was a successful test!') +message.set_text('This was a successful test!') +message.set_from('Elmer Thomas ') +status, msg = sg.send(message) +print status +print msg \ No newline at end of file diff --git a/example_v3_test.py b/example_v3_test.py new file mode 100755 index 000000000..71edba6c3 --- /dev/null +++ b/example_v3_test.py @@ -0,0 +1,76 @@ +import sendgrid +import json + +import os +if os.path.exists('.env'): + for line in open('.env'): + var = line.strip().split('=') + if len(var) == 2: + os.environ[var[0]] = var[1] + + + +client = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + +status, msg = client.asm_suppressions.delete(67,'elmer+test@thinkingserious.com') +print status +print msg + +""" + +status, msg = client.asm_suppressions.post(60, ['elmer+test@thinkingserious.com', 'elmer.thomas@yahoo.com']) +print status +print msg + + +status, msg = client.asm_suppressions.get(None,'elmer.thomas@yahoo.com') +print status +print msg + +status, msg = client.asm_groups.get([66,67,50]) +print status +print msg + +name = "My Amazing API Key" +status, msg = client.apikeys.post(name) +msg = json.loads(msg) +api_key_id = msg['api_key_id'] +print status +print msg + +name = "My NEW API Key 3000" +status, msg = client.apikeys.patch(api_key_id, name) +print status +print msg + +status, msg = client.apikeys.delete(api_key_id) +print status + +status, msg = client.apikeys.get() +print status +print msg + +# Get a list of all valid API Keys from your account +status, msg = client.apikeys.get() +print status +print msg + +# Create a new API Key +name = "My API Key 10" +status, msg = client.apikeys.post(name) +print status +print msg + +# Delete an API Key with a given api_key_id +api_key_id = "zc0r5sW5TTuBQGsMPMUx0A" +status, msg = client.apikeys.delete(api_key_id) +print status +print msg + +# Update the name of an API Key, given an api_key_id +api_key_id = "API_KEY" +name = "My API Key 3" +status, msg = client.apikeys.patch(api_key_id, name) +print status +print msg +""" \ No newline at end of file diff --git a/sendgrid/__init__.py b/sendgrid/__init__.py index 9fc719f78..270c2895e 100644 --- a/sendgrid/__init__.py +++ b/sendgrid/__init__.py @@ -1,4 +1,7 @@ from .version import __version__ from .sendgrid import SendGridClient from .exceptions import SendGridError, SendGridClientError, SendGridServerError +#v2 API from .message import Mail +#v3 API +from .client import SendGridAPIClient \ No newline at end of file diff --git a/sendgrid/client.py b/sendgrid/client.py new file mode 100644 index 000000000..02707e9d1 --- /dev/null +++ b/sendgrid/client.py @@ -0,0 +1,94 @@ +import json +from .version import __version__ +from socket import timeout +try: + import urllib.request as urllib_request + from urllib.parse import urlencode + from urllib.error import HTTPError +except ImportError: # Python 2 + import urllib2 as urllib_request + from urllib2 import HTTPError + from urllib import urlencode + +from .exceptions import SendGridClientError, SendGridServerError +from .resources.apikeys import APIKeys +from .resources.asm_groups import ASMGroups +from .resources.asm_suppressions import ASMSuppressions + +class SendGridAPIClient(object): + + """SendGrid API.""" + + def __init__(self, apikey, **opts): + """ + Construct SendGrid API object. + + Args: + apikey: SendGrid API key + opts: You can pass in host or proxies + """ + self._apikey = apikey + self.useragent = 'sendgrid/' + __version__ + ';python_v3' + self.host = opts.get('host', 'https://api.sendgrid.com') + # urllib cannot connect to SSL servers using proxies + self.proxies = opts.get('proxies', None) + + self.apikeys = APIKeys(self) + self.asm_groups = ASMGroups(self) + self.asm_suppressions = ASMSuppressions(self) + + @property + def apikey(self): + return self._apikey + + @apikey.setter + def apikey(self, value): + self._apikey = value + + def _build_request(self, url, json_header=False, method='GET', data=None): + if self.proxies: + proxy_support = urllib_request.ProxyHandler(self.proxies) + opener = urllib_request.build_opener(proxy_support) + urllib_request.install_opener(opener) + req = urllib_request.Request(url) + req.get_method = lambda: method + req.add_header('User-Agent', self.useragent) + req.add_header('Authorization', 'Bearer ' + self.apikey) + if json_header: + req.add_header('Content-Type', 'application/json') + try: + if data: + response = urllib_request.urlopen(req, json.dumps(data)) + else: + response = urllib_request.urlopen(req, timeout=10) + except HTTPError as e: + if 400 <= e.code < 500: + raise SendGridClientError(e.code, e.read()) + elif 500 <= e.code < 600: + raise SendGridServerError(e.code, e.read()) + else: + assert False + except timeout as e: + raise SendGridClientError(408, 'Request timeout') + body = response.read() + return response.getcode(), body + + def get(self, api): + url = self.host + api.endpoint + response, body = self._build_request(url, False, 'GET') + return response, body + + def post(self, api, data): + url = self.host + api.endpoint + response, body = self._build_request(url, True, 'POST', data) + return response, body + + def delete(self, api): + url = self.host + api.endpoint + response, body = self._build_request(url, False, 'DELETE') + return response, body + + def patch(self, api, data): + url = self.host + api.endpoint + response, body = self._build_request(url, True, 'PATCH', data) + return response, body diff --git a/sendgrid/message.py b/sendgrid/message.py index 4725f6a1e..b40dfa400 100644 --- a/sendgrid/message.py +++ b/sendgrid/message.py @@ -8,7 +8,7 @@ from smtpapi import SMTPAPIHeader -class Mail(object): +class Mail(): """SendGrid Message.""" @@ -165,7 +165,7 @@ def set_headers(self, headers): self.headers = json.loads(self.headers) if isinstance(headers, str): headers = json.loads(headers) - for key, value in headers.iteritems(): + for key, value in headers.items(): self.headers[key] = value def set_date(self, date): diff --git a/sendgrid/resources/__init__.py b/sendgrid/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sendgrid/resources/apikeys.py b/sendgrid/resources/apikeys.py new file mode 100644 index 000000000..e5de418b8 --- /dev/null +++ b/sendgrid/resources/apikeys.py @@ -0,0 +1,63 @@ +class APIKeys(object): + """The API Keys feature allows customers to be able to generate an API Key credential + which can be used for authentication with the SendGrid v3 Web API or the Mail API Endpoint""" + + def __init__(self, client, **opts): + """ + Constructs SendGrid APIKeys object. + + See https://sendgrid.com/docs/API_Reference/Web_API_v3/API_Keys/index.html + """ + self._name = None + self._base_endpoint = "/v3/api_keys" + self._endpoint = "/v3/api_keys" + self._client = client + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def base_endpoint(self): + return self._base_endpoint + + @property + def endpoint(self): + endpoint = self._endpoint + return endpoint + + @endpoint.setter + def endpoint(self, value): + self._endpoint = value + + @property + def client(self): + return self._client + + # Get a list of active API keys + def get(self): + return self.client.get(self) + + # Create a new API key with name (string) + def post(self, name): + data = {} + self.name = name + data['name'] = self.name + return self.client.post(self, data) + + # Delete a API key + def delete(self, api_key_id): + self.endpoint = self._base_endpoint + "/" + api_key_id + return self.client.delete(self) + + # Update a API key's name + def patch(self, api_key_id, name): + data = {} + self.name = name + data['name'] = self.name + self.endpoint = self._base_endpoint + "/" + api_key_id + return self.client.patch(self, data) \ No newline at end of file diff --git a/sendgrid/resources/asm_groups.py b/sendgrid/resources/asm_groups.py new file mode 100644 index 000000000..797f2c84f --- /dev/null +++ b/sendgrid/resources/asm_groups.py @@ -0,0 +1,56 @@ +class ASMGroups(object): + """Advanced Suppression Manager gives your recipients more control over the types of emails they want to receive + by letting them opt out of messages from a certain type of email. + + Groups are specific types of email you would like your recipients to be able to unsubscribe from or subscribe to. + For example: Daily Newsletters, Invoices, System Alerts. + """ + + def __init__(self, client, **opts): + """ + Constructs SendGrid ASM group object. + + See https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/index.html and + https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/groups.html + """ + self._name = None + self._base_endpoint = "/v3/asm/groups" + self._endpoint = "/v3/asm/groups" + self._client = client + + @property + def base_endpoint(self): + return self._base_endpoint + + @property + def endpoint(self): + endpoint = self._endpoint + return endpoint + + @endpoint.setter + def endpoint(self, value): + self._endpoint = value + + @property + def client(self): + return self._client + + # Retrieve all suppression groups associated with the user. + def get(self, id=None): + if id == None: + return self.client.get(self) + + if isinstance(id, int): + self._endpoint = self._base_endpoint + "/" + str(id) + return self.client.get(self) + + if len(id) > 1: + count = 0 + for i in id: + if count == 0: + self._endpoint = self._endpoint + "?id=" + str(i) + else: + self._endpoint = self._endpoint + "&id=" + str(i) + count = count + 1 + + return self.client.get(self) \ No newline at end of file diff --git a/sendgrid/resources/asm_suppressions.py b/sendgrid/resources/asm_suppressions.py new file mode 100644 index 000000000..820f0cd22 --- /dev/null +++ b/sendgrid/resources/asm_suppressions.py @@ -0,0 +1,63 @@ +class ASMSuppressions(object): + """Advanced Suppression Manager gives your recipients more control over the types of emails they want to receive + by letting them opt out of messages from a certain type of email. + + Suppressions are email addresses that can be added to groups to prevent certain types of emails from being + delivered to those addresses. + """ + + def __init__(self, client, **opts): + """ + Constructs SendGrid ASM suppressions object. + + See https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/index.html and + https://sendgrid.com/docs/API_Reference/Web_API_v3/Advanced_Suppression_Manager/groups.html + """ + self._name = None + self._base_endpoint = "/v3/asm/groups" + self._endpoint = "/v3/asm/groups" + self._client = client + + @property + def base_endpoint(self): + return self._base_endpoint + + @property + def endpoint(self): + endpoint = self._endpoint + return endpoint + + @endpoint.setter + def endpoint(self, value): + self._endpoint = value + + @property + def client(self): + return self._client + + # Get suppressed addresses for a given group id. + def get(self, id=None, email=None): + if id == None and email == None: + return self.client.get(self) + + if isinstance(id, int): + self._endpoint = self._base_endpoint + "/" + str(id) + "/suppressions" + return self.client.get(self) + + if isinstance(email, str): + self._endpoint = "/v3/asm/suppressions/" + email + + return self.client.get(self) + + # Add recipient addresses to the suppressions list for a given group. + # If the group has been deleted, this request will add the address to the global suppression. + def post(self, id, emails): + self._endpoint = self._base_endpoint + "/" + str(id) + "/suppressions" + data = {} + data["recipient_emails"] = emails + return self.client.post(self, data) + + # Delete a recipient email from the suppressions list for a group. + def delete(self, id, email): + self.endpoint = self._base_endpoint + "/" + str(id) + "/suppressions/" + email + return self.client.delete(self) \ No newline at end of file diff --git a/sendgrid/version.py b/sendgrid/version.py index 1097db477..7a5a23652 100644 --- a/sendgrid/version.py +++ b/sendgrid/version.py @@ -1,2 +1,2 @@ -version_info = (1, 4, 3) +version_info = (1, 5, 3) __version__ = '.'.join(str(v) for v in version_info) diff --git a/setup.py b/setup.py index 996684ce9..9fbb44d1f 100644 --- a/setup.py +++ b/setup.py @@ -17,8 +17,8 @@ def getRequires(): setup( name='sendgrid', version=str(__version__), - author='SendGrid', - author_email='libraries@sendgrid.com', + author='Yamil Asusta', + author_email='yamil@sendgrid.com', url='https://github.com/sendgrid/sendgrid-python/', packages=find_packages(), license='MIT', diff --git a/test/__init__.py b/test/__init__.py index 878c31089..e69de29bb 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,175 +0,0 @@ -import os -try: - import unittest2 as unittest -except ImportError: - import unittest -import json -import sys -import collections -try: - from StringIO import StringIO -except ImportError: # Python 3 - from io import StringIO - -from sendgrid import SendGridClient, Mail -from sendgrid.exceptions import SendGridClientError, SendGridServerError -from sendgrid.sendgrid import HTTPError - -SG_USER = os.getenv('SG_USER') or 'SENDGRID_USERNAME' -SG_PWD = os.getenv('SG_PWD') or 'SENDGRID_PASSWORD' - - -class TestSendGrid(unittest.TestCase): - def setUp(self): - self.sg = SendGridClient(SG_USER, SG_PWD) - - def test_apikey_init(self): - sg = SendGridClient(SG_PWD) - self.assertEqual(sg.password, SG_PWD) - self.assertIsNone(sg.username) - - @unittest.skipUnless(sys.version_info < (3, 0), 'only for python2') - def test_unicode_recipients(self): - recipients = [unicode('test@test.com'), unicode('guy@man.com')] - m = Mail(to=recipients, - subject='testing', - html='awesome', - from_email='from@test.com') - - mock = {'to[]': ['test@test.com', 'guy@man.com']} - result = self.sg._build_body(m) - - self.assertEqual(result['to[]'], mock['to[]']) - - def test_send(self): - m = Mail() - m.add_to('John, Doe ') - m.set_subject('test') - m.set_html('WIN') - m.set_text('WIN') - m.set_from('doe@email.com') - m.set_asm_group_id(42) - m.add_cc('cc@email.com') - m.add_bcc('bcc@email.com') - m.add_substitution('subKey', 'subValue') - m.add_section('testSection', 'sectionValue') - m.add_category('testCategory') - m.add_unique_arg('testUnique', 'uniqueValue') - m.add_filter('testFilter', 'filter', 'filterValue') - m.add_attachment_stream('testFile', 'fileValue') - m.set_replyto('John, Doe ') - url = self.sg._build_body(m) - url.pop('api_key', None) - url.pop('api_user', None) - url.pop('date', None) - test_url = json.loads(''' - { - "to[]": ["john@email.com"], - "toname[]": ["John Doe"], - "html": "WIN", - "text": "WIN", - "subject": "test", - "files[testFile]": "fileValue", - "from": "doe@email.com", - "cc[]": ["cc@email.com"], - "bcc[]": ["bcc@email.com"] - - } - ''') - test_url['headers'] = "{\"Reply-To\": \"John, Doe \"}" - - test_url['x-smtpapi'] = json.dumps(json.loads(''' - { - "sub": { - "subKey": ["subValue"] - }, - "section": { - "testSection":"sectionValue" - }, - "category": ["testCategory"], - "unique_args": { - "testUnique":"uniqueValue" - }, - "filters": { - "testFilter": { - "settings": { - "filter": "filterValue" - } - } - }, - "asm_group_id": 42 - } - ''')) - - self.assertEqual(url, test_url) - - @unittest.skipUnless(sys.version_info < (3, 0), 'only for python2') - def test__build_body_unicode(self): - """test _build_body() handles encoded unicode outside ascii range""" - from_email = '\xd0\x9d\xd0\xb8\xd0\xba\xd0\xb0@email.com' - from_name = '\xd0\x9a\xd0\xbb\xd0\xb0\xd0\xb2\xd0\xb4\xd0\xb8\xd1\x8f' - subject = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' - text = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' - html = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' - m = Mail() - m.add_to('John, Doe ') - m.set_subject(subject) - m.set_html(html) - m.set_text(text) - m.set_from("%s <%s>" % (from_name, from_email)) - url = self.sg._build_body(m) - self.assertEqual(from_email, url['from']) - self.assertEqual(from_name, url['fromname']) - self.assertEqual(subject, url['subject']) - self.assertEqual(text, url['text']) - self.assertEqual(html, url['html']) - - def test_smtpapi_add_to(self): - '''Test that message.to gets a dummy address for the header to work''' - m = Mail() - m.smtpapi.add_to('test@email.com') - m.set_from('jon@doe.com') - m.set_subject('test') - url = self.sg._build_body(m) - url.pop('api_key', None) - url.pop('api_user', None) - url.pop('date', None) - test_url = json.loads(''' - { - "to[]": ["jon@doe.com"], - "subject": "test", - "from": "jon@doe.com" - } - ''') - test_url['x-smtpapi'] = json.dumps(json.loads(''' - { - "to": ["test@email.com"] - } - ''')) - self.assertEqual(url, test_url) - - -class SendGridClientUnderTest(SendGridClient): - - def _make_request(self, message): - raise self.error - - -class TestSendGridErrorHandling(unittest.TestCase): - - def setUp(self): - self.sg = SendGridClientUnderTest(SG_USER, SG_PWD, raise_errors=True) - - def test_client_raises_clinet_error_in_case_of_4xx(self): - self.sg.error = HTTPError('url', 403, 'msg', {}, StringIO('body')) - with self.assertRaises(SendGridClientError): - self.sg.send(Mail()) - - def test_client_raises_clinet_error_in_case_of_5xx(self): - self.sg.error = HTTPError('url', 503, 'msg', {}, StringIO('body')) - with self.assertRaises(SendGridServerError): - self.sg.send(Mail()) - - -if __name__ == '__main__': - unittest.main() diff --git a/test/base_test.py b/test/base_test.py new file mode 100644 index 000000000..6e0979998 --- /dev/null +++ b/test/base_test.py @@ -0,0 +1,37 @@ +import sendgrid +from sendgrid.client import SendGridAPIClient +try: + import urllib.request as urllib_request + from urllib.parse import urlencode + from urllib.error import HTTPError +except ImportError: # Python 2 + import urllib2 as urllib_request + from urllib2 import HTTPError + from urllib import urlencode + +class BaseTest(): + def __init__(self): + pass + +class MockSendGridAPIClientRequest(SendGridAPIClient): + def __init__(self, apikey, **opts): + super(MockSendGridAPIClientRequest, self).__init__(apikey, **opts) + self._req = None + + def _build_request(self, url=None, json_header=False, method='GET', data=None): + req = urllib_request.Request(url) + req.get_method = lambda: method + req.add_header('User-Agent', self.useragent) + req.add_header('Authorization', 'Bearer ' + self.apikey) + if json_header: + req.add_header('Content-Type', 'application/json') + body = data + if method == 'POST': + response = 201 + if method == 'PATCH': + response = 200 + if method == 'DELETE': + response = 204 + if method == 'GET': + response = 200 + return response, body \ No newline at end of file diff --git a/test/test_api_client.py b/test/test_api_client.py new file mode 100644 index 000000000..4e1486671 --- /dev/null +++ b/test/test_api_client.py @@ -0,0 +1,35 @@ +from .base_test import BaseTest, MockSendGridAPIClientRequest +import os +try: + import unittest2 as unittest +except ImportError: + import unittest +try: + from StringIO import StringIO +except ImportError: # Python 3 + from io import StringIO + +import sendgrid +from sendgrid.client import SendGridAPIClient +from sendgrid.version import __version__ + +SG_KEY = os.getenv('SG_KEY') or 'SENDGRID_APIKEY' + +class TestSendGridAPIClient(unittest.TestCase): + def setUp(self): + self.client = MockSendGridAPIClientRequest + self.client = SendGridAPIClient(SG_KEY) + + def test_apikey_init(self): + self.assertEqual(self.client.apikey, SG_KEY) + + def test_useragent(self): + useragent = 'sendgrid/' + __version__ + ';python_v3' + self.assertEqual(self.client.useragent, useragent) + + def test_host(self): + host = 'https://api.sendgrid.com' + self.assertEqual(self.client.host, host) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_apikeys.py b/test/test_apikeys.py new file mode 100644 index 000000000..c7a0cc96f --- /dev/null +++ b/test/test_apikeys.py @@ -0,0 +1,51 @@ +from .base_test import BaseTest, MockSendGridAPIClientRequest +import os +try: + import unittest2 as unittest +except ImportError: + import unittest +try: + from StringIO import StringIO +except ImportError: # Python 3 + from io import StringIO + +import sendgrid +from sendgrid.client import SendGridAPIClient +from sendgrid.version import __version__ + +SG_KEY = os.getenv('SG_KEY') or 'SENDGRID_APIKEY' + +class TestAPIKeys(unittest.TestCase): + def setUp(self): + SendGridAPIClient = MockSendGridAPIClientRequest + self.client = SendGridAPIClient(SG_KEY) + + def test_apikeys_init(self): + self.apikeys = self.client.apikeys + self.assertEqual(self.apikeys.name, None) + self.assertEqual(self.apikeys.base_endpoint, "/v3/api_keys") + self.assertEqual(self.apikeys.endpoint, "/v3/api_keys") + self.assertEqual(self.apikeys.client, self.client) + + def test_apikeys_post(self): + name = "My Amazing API Key of Wonder [PATCH Test]" + status, msg = self.client.apikeys.post(name) + self.assertEqual(status, 201) + self.assertEqual(msg['name'], name) + + def test_apikeys_patch(self): + name = "My NEW Amazing API Key of Wonder [PATCH TEST]" + status, msg = self.client.apikeys.patch(SG_KEY, name) + self.assertEqual(status, 200) + self.assertEqual(msg['name'], name) + + def test_apikeys_delete(self): + status, msg = self.client.apikeys.delete(SG_KEY) + self.assertEqual(status, 204) + + def test_apikeys_get(self): + status, msg = self.client.apikeys.get() + self.assertEqual(status, 200) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_asm_groups.py b/test/test_asm_groups.py new file mode 100644 index 000000000..83a795b2c --- /dev/null +++ b/test/test_asm_groups.py @@ -0,0 +1,34 @@ +from .base_test import BaseTest, MockSendGridAPIClientRequest +import os +try: + import unittest2 as unittest +except ImportError: + import unittest +try: + from StringIO import StringIO +except ImportError: # Python 3 + from io import StringIO + +import sendgrid +from sendgrid.client import SendGridAPIClient +from sendgrid.version import __version__ + +SG_KEY = os.getenv('SG_KEY') or 'SENDGRID_APIKEY' + +class TestASMGroups(unittest.TestCase): + def setUp(self): + SendGridAPIClient = MockSendGridAPIClientRequest + self.client = SendGridAPIClient(SG_KEY) + + def test_asm_groups_init(self): + self.asm_groups = self.client.asm_groups + self.assertEqual(self.asm_groups.base_endpoint, "/v3/asm/groups") + self.assertEqual(self.asm_groups.endpoint, "/v3/asm/groups") + self.assertEqual(self.asm_groups.client, self.client) + + def test_asm_groups_get(self): + status, msg = self.client.asm_groups.get() + self.assertEqual(status, 200) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_asm_suppressions.py b/test/test_asm_suppressions.py new file mode 100644 index 000000000..9ce52a43c --- /dev/null +++ b/test/test_asm_suppressions.py @@ -0,0 +1,51 @@ +from .base_test import BaseTest, MockSendGridAPIClientRequest +import os +try: + import unittest2 as unittest +except ImportError: + import unittest +try: + from StringIO import StringIO +except ImportError: # Python 3 + from io import StringIO + +import sendgrid +from sendgrid.client import SendGridAPIClient +from sendgrid.version import __version__ + +SG_KEY = os.getenv('SG_KEY') or 'SENDGRID_APIKEY' + +class TestASMGroups(unittest.TestCase): + def setUp(self): + SendGridAPIClient = MockSendGridAPIClientRequest + self.client = SendGridAPIClient(SG_KEY) + + def test_asm_suppressions_init(self): + self.asm_suppressions = self.client.asm_suppressions + self.assertEqual(self.asm_suppressions.base_endpoint, "/v3/asm/groups") + self.assertEqual(self.asm_suppressions.endpoint, "/v3/asm/groups") + self.assertEqual(self.asm_suppressions.client, self.client) + + def test_asm_suppressions_get(self): + status, msg = self.client.asm_suppressions.get() + self.assertEqual(status, 200) + + def test_asm_suppressions_post(self): + id = 67 + emails = ['elmer+test@thinkingserious.com'] + status, msg = self.client.asm_suppressions.post(id, emails) + self.assertEqual(status, 201) + self.assertEqual(msg['recipient_emails'], emails) + emails = ['elmer+test@thinkingserious.com', 'elmer.thomas@yahoo.com'] + status, msg = self.client.asm_suppressions.post(id, emails) + self.assertEqual(status, 201) + self.assertEqual(msg['recipient_emails'], emails) + + def test_asm_supressions_delete(self): + id = 67 + email = 'elmer+test@thinkingserious.com' + status, msg = self.client.asm_suppressions.delete(id, email) + self.assertEqual(status, 204) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/test_mail_v2.py b/test/test_mail_v2.py new file mode 100644 index 000000000..7038d3663 --- /dev/null +++ b/test/test_mail_v2.py @@ -0,0 +1,165 @@ +import os +try: + import unittest2 as unittest +except ImportError: + import unittest +import json +import sys +import collections +try: + from StringIO import StringIO +except ImportError: # Python 3 + from io import StringIO + +from sendgrid import SendGridClient, Mail +from sendgrid.exceptions import SendGridClientError, SendGridServerError +from sendgrid.sendgrid import HTTPError + +SG_USER = os.getenv('SG_USER') or 'SENDGRID_USERNAME' +SG_PWD = os.getenv('SG_PWD') or 'SENDGRID_PASSWORD' + +class TestSendGrid(unittest.TestCase): + + def setUp(self): + self.sg = SendGridClient(SG_USER, SG_PWD) + + def test_apikey_init(self): + sg = SendGridClient(SG_PWD) + self.assertEqual(sg.password, SG_PWD) + self.assertIsNone(sg.username) + + @unittest.skipUnless(sys.version_info < (3, 0), 'only for python2') + def test_unicode_recipients(self): + recipients = [unicode('test@test.com'), unicode('guy@man.com')] + m = Mail(to=recipients, + subject='testing', + html='awesome', + from_email='from@test.com') + + mock = {'to[]': ['test@test.com', 'guy@man.com']} + result = self.sg._build_body(m) + + self.assertEqual(result['to[]'], mock['to[]']) + + def test_send(self): + m = Mail() + m.add_to('John, Doe ') + m.set_subject('test') + m.set_html('WIN') + m.set_text('WIN') + m.set_from('doe@email.com') + m.set_asm_group_id(42) + m.add_cc('cc@email.com') + m.add_bcc('bcc@email.com') + m.add_substitution('subKey', 'subValue') + m.add_section('testSection', 'sectionValue') + m.add_category('testCategory') + m.add_unique_arg('testUnique', 'uniqueValue') + m.add_filter('testFilter', 'filter', 'filterValue') + m.add_attachment_stream('testFile', 'fileValue') + url = self.sg._build_body(m) + url.pop('api_key', None) + url.pop('api_user', None) + url.pop('date', None) + test_url = json.loads(''' + { + "to[]": ["john@email.com"], + "toname[]": ["John Doe"], + "html": "WIN", + "text": "WIN", + "subject": "test", + "files[testFile]": "fileValue", + "from": "doe@email.com", + "cc[]": ["cc@email.com"], + "bcc[]": ["bcc@email.com"] + } + ''') + test_url['x-smtpapi'] = json.dumps(json.loads(''' + { + "sub": { + "subKey": ["subValue"] + }, + "section": { + "testSection":"sectionValue" + }, + "category": ["testCategory"], + "unique_args": { + "testUnique":"uniqueValue" + }, + "filters": { + "testFilter": { + "settings": { + "filter": "filterValue" + } + } + }, + "asm_group_id": 42 + } + ''')) + + self.assertEqual(url, test_url) + + @unittest.skipUnless(sys.version_info < (3, 0), 'only for python2') + def test__build_body_unicode(self): + """test _build_body() handles encoded unicode outside ascii range""" + from_email = '\xd0\x9d\xd0\xb8\xd0\xba\xd0\xb0@email.com' + from_name = '\xd0\x9a\xd0\xbb\xd0\xb0\xd0\xb2\xd0\xb4\xd0\xb8\xd1\x8f' + subject = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' + text = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' + html = '\xd0\x9d\xd0\xb0\xd0\xb4\xd0\xb5\xd0\xb6\xd0\xb4\xd0\xb0' + m = Mail() + m.add_to('John, Doe ') + m.set_subject(subject) + m.set_html(html) + m.set_text(text) + m.set_from("%s <%s>" % (from_name, from_email)) + url = self.sg._build_body(m) + self.assertEqual(from_email, url['from']) + self.assertEqual(from_name, url['fromname']) + self.assertEqual(subject, url['subject']) + self.assertEqual(text, url['text']) + self.assertEqual(html, url['html']) + + + def test_smtpapi_add_to(self): + '''Test that message.to gets a dummy address for the header to work''' + m = Mail() + m.smtpapi.add_to('test@email.com') + m.set_from('jon@doe.com') + m.set_subject('test') + url = self.sg._build_body(m) + url.pop('api_key', None) + url.pop('api_user', None) + url.pop('date', None) + test_url = json.loads(''' + { + "to[]": ["jon@doe.com"], + "subject": "test", + "from": "jon@doe.com" + } + ''') + test_url['x-smtpapi'] = json.dumps(json.loads(''' + { + "to": ["test@email.com"] + } + ''')) + self.assertEqual(url, test_url) + +class SendGridClientUnderTest(SendGridClient): + + def _make_request(self, message): + raise self.error + +class TestSendGridErrorHandling(unittest.TestCase): + def setUp(self): + self.sg = SendGridClientUnderTest(SG_USER, SG_PWD, raise_errors=True) + + def test_client_raises_clinet_error_in_case_of_4xx(self): + self.sg.error = HTTPError('url', 403, 'msg', {}, StringIO('body')) + with self.assertRaises(SendGridClientError): + self.sg.send(Mail()) + + def test_client_raises_clinet_error_in_case_of_5xx(self): + self.sg.error = HTTPError('url', 503, 'msg', {}, StringIO('body')) + with self.assertRaises(SendGridServerError): + self.sg.send(Mail()) \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..a8b9b53ad --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py26, py27, py32 + +[testenv] +commands = {envbindir}/python -m unittest discover -v [] +deps = + +[testenv:py26] +commands = {envbindir}/unit2 discover -v [] +deps = unittest2 +basepython = python2.6 + +[testenv:py27] +commands = {envbindir}/python -m unittest discover -v [] +deps = +basepython = python2.7 + +[testenv:py32] +commands = {envbindir}/python -m unittest discover -v [] +deps = +basepython = python3.2 \ No newline at end of file