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 IMAGE
AFTER 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