diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000..c69968e --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,7 @@ +### Client version + +### Expected behaviour + +### Actual behaviour + +### Steps to reproduce diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000..3e198cf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,28 @@ + + +## Description + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. diff --git a/.gitignore b/.gitignore index 7316d36..ad4b0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,15 +8,15 @@ local_settings.py # pypi building stuff MANIFEST +.cache/ dist/ dist/* build/ build/* *.egg-info -# IntelliJ -.idea -*.iml - # testing/tox .tox/ + +# virtualenv +.venv diff --git a/.travis.yml b/.travis.yml index e77e1bf..4fb7ad3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,24 @@ language: python -python: - - "2.6" - - "2.7" - - "3.4" - - "3.5" -install: "pip install -r requirements.txt" +matrix: + include: + - python: 2.6 + env: + - TOXENV='py26' + - python: 2.7 + env: + - TOXENV='py27' + - python: 3.3 + env: + - TOXENV='py33' + - python: 3.5 + env: + - TOXENV='py35' + - python: 3.5 + env: + - TOXENV='lint' -script: python setup.py test +install: + - pip install tox + +script: tox diff --git a/README.md b/README.md index f58f9ec..7ec8d8c 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ sendwithus python-client [](https://travis-ci.org/sendwithus/sendwithus_python) -## requirements -python requests library +## Requirements +- [Python requests library](http://docs.python-requests.org/en/master/user/install/#install) -## installation +## Installation pip install sendwithus -## usage +## Usage For all examples, assume: ```python @@ -17,24 +17,24 @@ import sendwithus api = sendwithus.api(api_key='YOUR-API-KEY') ``` -### error handling -By default, the api calls return a response object. However, you can use +### Error Handling +By default, the API calls return a response object. However, you can use `sendwithus.api(api_key='YOUR-API-KEY', raise_errors=True)` which will raise the following errors: -* `AuthenticationError` - Caused by an invalid api key -* `APIError` - Caused by an invalid api request (4xx error) +* `AuthenticationError` - Caused by an invalid API key +* `APIError` - Caused by an invalid API request (4xx error) * `ServerError` - Caused by a server error (5xx error) Errors can be imported from the `sendwithus.exceptions` module. # Templates -## Get your templates +### Get your Templates ```python api.templates() ``` -## Create a template +### Create a Template ```python api.create_template( @@ -57,29 +57,29 @@ r.content *NOTE* - If a customer does not exist by the specified email (recipient address), the send call will create a customer. -- email_id -- Template ID to send +- email_id — Template ID to send - recipient - - address -- The recipient's email address - - name (optional) -- The recipient's name -- email_data -- Object containing email template data + - address — The recipient's email address + - name (optional) — The recipient's name +- email_data (optional) — Object containing email template data - sender (optional) - - address -- The sender's email address - - reply_to -- The sender's reply-to address - - name -- The sender's name -- cc (optional) -- A list of CC recipients, of the format {"address":"cc@email.com"} -- bcc (optional) -- A list of BCC recipients, of the format {"address":"bcc@email.com"} -- headers (options) -- Object contain SMTP headers to be included with the email -- esp\_account (optional) -- ID of the ESP Account to send this email through. ex: esp\_1a2b3c4d5e -- files (optional) -- List of file attachments (combined maximum 7MB) -- inline (optional) -- Inline attachment object -- locale (optional) -- Template locale to send (ie: en-US) + - address — The sender's email address + - reply_to — The sender's reply-to address + - name — The sender's name +- cc (optional) — A list of CC recipients, of the format {"address":"cc@email.com"} +- bcc (optional) — A list of BCC recipients, of the format {"address":"bcc@email.com"} +- headers (options) — Object contain SMTP headers to be included with the email +- esp\_account (optional) — ID of the ESP Account to send this email through. ex: esp\_1a2b3c4d5e +- files (optional) — List of file attachments (combined maximum 7MB) +- inline (optional) — Inline attachment object +- locale (optional) — Template locale to send (ie: en-US) ### Call with REQUIRED parameters only The `email_data` field is optional, but highly recommended! ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'address': 'us@sendwithus.com'}) print r.status_code # 200 @@ -88,7 +88,7 @@ print r.status_code ### Call with REQUIRED parameters and email_data ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'address': 'us@sendwithus.com'}, email_data={ 'first_name': 'Matt' }) print r.status_code @@ -100,7 +100,7 @@ The `sender['address']` is a required sender field ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={ 'name': 'Matt', 'address': 'us@sendwithus.com'}, email_data={ 'first_name': 'Matt' }, @@ -114,7 +114,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={ 'name': 'Matt', 'address': 'us@sendwithus.com'}, email_data={ 'first_name': 'Matt' }, @@ -129,7 +129,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, cc=[ @@ -144,7 +144,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, bcc=[ @@ -159,7 +159,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, headers={'X-HEADER-ONE': 'header-value'}) @@ -171,7 +171,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, esp_account='esp_1234asdf1234') @@ -183,7 +183,7 @@ print r.status_code ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, files=[open('/home/Matt/report1.txt', 'r'), open('/home/Matt/report2.txt', 'r')]) @@ -191,11 +191,24 @@ print r.status_code # 200 ``` +### Optional File Attachments with explicit file names + +```python +r = api.send( + email_id='YOUR-TEMPLATE-ID', + recipient={'name': 'Matt', + 'address': 'us@sendwithus.com'}, + files=[{'file': open('/home/Matt/report1.txt', 'r'), + 'filename': 'arbitrary_file_name.xyz'}]) +print r.status_code +# 200 +``` + ### Optional Inline Image ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, inline=open('image.jpg', 'r')) @@ -203,10 +216,24 @@ print r.status_code # 200 ``` +### Optional Inline Image with explicit file names + +```python +r = api.send( + email_id='YOUR-TEMPLATE-ID', + recipient={'name': 'Matt', + 'address': 'us@sendwithus.com'}, + inline={'file': open('/home/Matt/image.jpg, 'r'), + 'filename': 'cool_image.jpg'}) +print r.status_code +# 200 +``` + + ### Optional Locale ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, locale='en-US') @@ -216,7 +243,7 @@ print r.status_code # Drip Campaigns -## List all drip campaigns +### List all Drip Campaigns List all drip campaigns for the current profile @@ -224,7 +251,7 @@ List all drip campaigns for the current profile api.list_drip_campaigns() ``` -## Start a customer on a drip campaign +### Start a Customer on a Drip Campaign Starts a customer on the first step of a specified drip campaign @@ -232,9 +259,11 @@ Starts a customer on the first step of a specified drip campaign api.start_on_drip_campaign('dc_1234asdf1234', {'address':'customer@email.com'}) ``` -### Extra Data +### Start a Customer on a Drip Campaign with email_data -You may specify extra data to be merged into the templates in the drip campaign +You may specify extra data to be merged into the templates in the drip campaign. + +*Note* — Any data provided in the `email_data` parameter for `start_on_drip_campaign()` will be used throughout the entire drip campaign. ```python api.start_on_drip_campaign( @@ -249,7 +278,7 @@ api.start_on_drip_campaign( ) ``` -## Remove a customer from a drip campaign +### Remove a Customer from a Drip Campaign Deactivates all pending emails for a customer on a specified drip campaign @@ -257,7 +286,7 @@ Deactivates all pending emails for a customer on a specified drip campaign api.remove_from_drip_campaign('customer@email.com', 'dc_1234asdf1234') ``` -## Remove a customer from all drip campaigns +### Remove a Customer from all Drip Campaigns You can deactivate all pending drip campaign emails for a customer @@ -265,7 +294,7 @@ You can deactivate all pending drip campaign emails for a customer api.drip_deactivate('customer@example.com') ``` -## List the details of a specific campaign +### List the details of a specific Drip Campaign ```python api.drip_campaign_details('dc_1234asdf1234') @@ -273,13 +302,13 @@ api.drip_campaign_details('dc_1234asdf1234') # Customers -## Get a Customer +### Get a Customer ```python api.customer_details('customer@example.com') ``` -## Create/update Customer +### Create/Update Customer You can use the same endpoint to create or update a customer. Sendwithus will perform a merge of the data on the customer profile, preferring the new data. @@ -289,126 +318,105 @@ api.customer_create('customer@example.com', data={'first_name': 'Matt'}) ``` -## Delete a Customer +### Delete a Customer ```python api.customer_delete('customer@example.com') ``` -## Add Customer to a Group -```python -api.add_customer_to_group('customer@example.com', 'grp_1234') -``` +# Render + +### Render a Template with data -## Remove Customer from a Group +The Render API allows you to render a template with data, using the exact same rendering workflow that Sendwithus uses when delivering your email. +`Strict` is set to `False` as a default, if `Strict=True` this API call will fail on any missing `email_data`. ```python -api.remove_customer_from_group('customer@example.com', 'grp_1234') +api.render('tem_12345', { "amount": "$12.00" }, locale='fr-FR', version_name='French-Version', strict=False) ``` -# Conversions +### Expected Response -## Create a customer conversion event +#### Success +```bash + >>> r.status_code + 200 -You can use the Conversion API to track conversion and revenue data events -against your sent emails. + >>> r.json().get('success') + True -**NOTE:** Revenue is in cents (eg. $100.50 = 10050) + >>> r.json().get('status') + u'OK' -```python -api.customer_conversion('customer@example.com', revenue=10050) + >>> r.json().get('receipt_id') + u'numeric-receipt-id' ``` -# Customer Groups - -## Create a Customer Group - -```python -api.create_customer_group('group_name', 'sample group description') +#### Error cases +* malformed request +```bash + >>> r.status_code + 400 ``` -## Delete a customer group - -```python -api.delete_customer_group('grp_1234') +* bad API key +```bash + >>> r.status_code + 403 ``` -## Update a Customer Group +## Run Tests +Use [tox](https://tox.readthedocs.io/en/latest/) to run the tests: -```python -api.update_customer_group('new_name', 'updated group description') +```bash +tox ``` -# Segmentation - -## Send Template to Segment +### Testing Multiple Python Versions +This assumes you have [tox](https://tox.readthedocs.io/en/latest/) installed and used +[pyenv](https://github.com/yyuu/pyenv) to install multiple versions of python. -You can use the Segments API to send a template to all customers who match a -segment. The Segment must be created in the Sendwithus dashboard, which is -where you will find the `segment_id` for use in this API. +Once all the supported python versions are installed simply run: -```python -api.send_segment('tem_12345', 'seg_1245') +```bash +tox ``` -### Extra Data +This will run the tests against all the versions specified in `tox.ini`. -You may specify extra data to be merged into the template, alongside the -individual customer profiles +## Troubleshooting -```python -api.send_segment('tem_12345', 'seg_12345', email_data={'color': 'blue'}) -``` +### General Troubleshooting -# Render +- Enable debug mode +- Make sure you're using the latest Python client +- Capture the response data and check your logs — often this will have the exact error -## Render a Template with data +### Enable Debug Mode -The render API allows you to render a template with data, using the exact same rendering workflow that Sendwithus uses when delivering your email. +Debug mode prints out the underlying request information as well as the data payload that gets sent to Sendwithus. You will most likely find this information in your logs. To enable it, simply put `DEBUG=True` as a parameter when instantiating the API object. Use the debug mode to compare the data payload getting sent to [Sendwithus' API docs](https://www.sendwithus.com/docs/api "Official Sendwithus API Docs"). ```python -api.render('tem_12345', { "amount": "$12.00" }, 'French-Version', strict=False) +import sendwithus +api = sendwithus.api(api_key='YOUR-API-KEY', DEBUG=True) ``` +### Response Ranges -## expected response - -### Success - >>> r.status_code - 200 - - >>> r.json().get('success') - True - - >>> r.json().get('status') - u'OK' - - >>> r.json().get('receipt_id') - u'numeric-receipt-id' +Sendwithus' API typically sends responses back in these ranges: -### Error cases -* malformed request +- 2xx – Successful Request +- 4xx – Failed Request (Client error) +- 5xx – Failed Request (Server error) - >>> r.status_code - 400 - -* bad API key +If you're receiving an error in the 400 response range follow these steps: - >>> r.status_code - 403 +- Double check the data and ID's getting passed to Sendwithus +- Ensure your API key is correct +- Log and check the body of the response -## to run tests - python setup.py test - -### Testing multiple python versions -This assumes you have [tox](https://testrun.org/tox/latest/) installed and used -[pyenv](https://github.com/yyuu/pyenv) to install multiple versions of python. - -Once all the supported python versions are installed simply run: - - tox - -This will run the tests against all the versions specified in `tox.ini`. - -### packaging (internal) - python setup.py sdist upload +### Internal +To package +```bash + python setup.py sdist bdist_wheel upload +``` diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..1490513 --- /dev/null +++ b/conftest.py @@ -0,0 +1,87 @@ +import pytest +import sendwithus + + +@pytest.fixture +def api_key(): + return 'THIS_IS_A_TEST_API_KEY' + + +@pytest.fixture +def api_options(): + return {'DEBUG': False} + + +@pytest.fixture +def api(api_key, api_options): + return sendwithus.api(api_key, **api_options) + + +@pytest.fixture +def email_id(): + return 'test_fixture_1' + + +@pytest.fixture +def email_address(): + return 'person@example.com' + + +@pytest.fixture +def enabled_drip_campaign_id(): + return 'dc_Rmd7y5oUJ3tn86sPJ8ESCk' + + +@pytest.fixture +def disabled_drip_campaign_id(): + return 'dc_AjR6Ue9PHPFYmEu2gd8x5V' + + +@pytest.fixture +def drip_campaign_step_id(): + return 'dcs_yaAMiZNWCLAEGw7GLjBuGY' + + +@pytest.fixture +def recipient(): + return { + 'name': 'Matt', + 'address': 'us@sendwithus.com' + } + + +@pytest.fixture +def email_data(): + return { + 'name': 'Jimmy', + 'plants': ['Tree', 'Bush', 'Shrub'] + } + + +@pytest.fixture +def sender(): + return { + 'name': 'Company', + 'address': 'company@company.com', + 'reply_to': 'info@company.com' + } + + +@pytest.fixture +def cc_test(): + return [ + { + 'name': 'Matt CC', + 'address': 'test+cc@sendwithus.com' + } + ] + + +@pytest.fixture +def bcc_test(): + return [ + { + 'name': 'Matt BCC', + 'address': 'test+bcc@sendwithus.com' + } + ] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f91a27a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests==2.0.0 -six==1.9.0 diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index dbaa36e..7c06125 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -4,16 +4,17 @@ """ import base64 -import logging import json +import logging +import warnings + import requests +import six from six import string_types -import warnings from .encoder import SendwithusJSONEncoder -from .version import version from .exceptions import APIError, AuthenticationError, ServerError - +from .version import version LOGGER_FORMAT = '%(asctime)-15s %(message)s' logger = logging.getLogger('sendwithus') @@ -39,29 +40,24 @@ class api: TEMPLATES_ENDPOINT = 'templates' TEMPLATES_SPECIFIC_ENDPOINT = 'templates/%s' TEMPLATES_LOCALES_ENDPOINT = 'templates/%s/locales' - TEMPLATES_SPECIFIC_LOCALE_VERSIONS_ENDPOINT = 'templates/%s/locales/%s/versions' + TEMPLATES_SPECIFIC_LOCALE_VERSIONS_ENDPOINT = \ + 'templates/%s/locales/%s/versions' TEMPLATES_NEW_VERSION_ENDPOINT = 'templates/%s/versions' TEMPLATES_VERSION_ENDPOINT = 'templates/%s/versions/%s' SNIPPETS_ENDPOINT = 'snippets' SNIPPET_ENDPOINT = 'snippets/%s' SEND_ENDPOINT = 'send' - SEGMENTS_ENDPOINT = 'segments' - RUN_SEGMENT_ENDPOINT = 'segments/%s/run' - SEND_SEGMENT_ENDPOINT = 'segments/%s/send' DRIPS_DEACTIVATE_ENDPOINT = 'drips/deactivate' CUSTOMER_CREATE_ENDPOINT = 'customers' CUSTOMER_DETAILS_ENDPOINT = 'customers/%s' CUSTOMER_DELETE_ENDPOINT = 'customers/%s' - CUSTOMER_CONVERSION_ENDPOINT = 'customers/%s/conversions' - CUSTOMER_GROUPS_ENDPOINT = 'customers/%s/groups/%s' - GROUPS_ENDPOINT = 'groups' - GROUP_ENDPOINT = 'groups/%s' DRIP_CAMPAIGN_LIST_ENDPOINT = 'drip_campaigns' DRIP_CAMPAIGN_ACTIVATE_ENDPOINT = 'drip_campaigns/%s/activate' DRIP_CAMPAIGN_DEACTIVATE_ENDPOINT = 'drip_campaigns/%s/deactivate' DRIP_CAMPAIGN_DETAILS_ENDPOINT = 'drip_campaigns/%s' DRIP_CAMPAIGN_CUSTOMERS_ENDPOINT = 'drip_campaigns/%s/customers' - DRIP_CAMPAIGN_STEP_CUSTOMERS_ENDPOINT = 'drip_campaigns/%s/steps/%s/customers' + DRIP_CAMPAIGN_STEP_CUSTOMERS_ENDPOINT = \ + 'drip_campaigns/%s/steps/%s/customers' BATCH_ENDPOINT = 'batch' RENDER_ENDPOINT = 'render' @@ -70,14 +66,23 @@ class api: API_KEY = 'THIS_IS_A_TEST_API_KEY' DEBUG = False - - def __init__(self, api_key=None, json_encoder=SendwithusJSONEncoder, raise_errors=False, **kwargs): + DEFAULT_TIMEOUT = None + + def __init__( + self, + api_key=None, + json_encoder=SendwithusJSONEncoder, + raise_errors=False, + default_timeout=None, + **kwargs + ): """Constructor, expects api key""" if not api_key: raise Exception("You must specify an api key") self.API_KEY = api_key + self.DEFAULT_TIMEOUT = default_timeout self._json_encoder = json_encoder self._raise_errors = raise_errors @@ -101,7 +106,10 @@ def _build_http_auth(self): return (self.API_KEY, '') def _build_request_headers(self, custom_headers=None): - client_header = '%s-%s' % (self.API_CLIENT_LANG, self.API_CLIENT_VERSION) + client_header = '%s-%s' % ( + self.API_CLIENT_LANG, + self.API_CLIENT_VERSION + ) headers = { self.API_HEADER_CLIENT: client_header, @@ -117,7 +125,12 @@ def _build_request_headers(self, custom_headers=None): def _build_request_path(self, endpoint, absolute=True): path = '/api/v%s/%s' % (self.API_VERSION, endpoint) if absolute: - path = "%s://%s:%s%s" % (self.API_PROTO, self.API_HOST, self.API_PORT, path) + path = "%s://%s:%s%s" % ( + self.API_PROTO, + self.API_HOST, + self.API_PORT, + path + ) return path def _build_payload(self, data): @@ -126,7 +139,9 @@ def _build_payload(self, data): return json.dumps(data, cls=self._json_encoder) def _parse_response(self, response): - """Parses the API response and raises appropriate errors if raise_errors was set to True""" + """Parses the API response and raises appropriate errors if + raise_errors was set to True + """ if not self._raise_errors: return response @@ -158,21 +173,27 @@ def _api_request(self, endpoint, http_method, *args, **kwargs): data = self._build_payload(kwargs.get('payload')) logger.debug('\tdata: %s' % data) + req_kw = dict( + auth=auth, + headers=headers, + timeout=kwargs.get('timeout', self.DEFAULT_TIMEOUT) + ) + # do some error handling if (http_method == self.HTTP_POST): if (data): - r = requests.post(path, auth=auth, data=data, headers=headers) + r = requests.post(path, data=data, **req_kw) else: - r = requests.post(path, auth=auth, headers=headers) + r = requests.post(path, **req_kw) elif http_method == self.HTTP_PUT: if (data): - r = requests.put(path, auth=auth, data=data, headers=headers) + r = requests.put(path, data=data, **req_kw) else: - r = requests.put(path, auth=auth, headers=headers) + r = requests.put(path, **req_kw) elif http_method == self.HTTP_DELETE: - r = requests.delete(path, auth=auth, headers=headers) + r = requests.delete(path, **req_kw) else: - r = requests.get(path, auth=auth, headers=headers) + r = requests.get(path, **req_kw) logger.debug('\tresponse code:%s' % r.status_code) try: @@ -182,39 +203,69 @@ def _api_request(self, endpoint, http_method, *args, **kwargs): return self._parse_response(r) - def logs(self): + def logs(self, timeout=None): """ API call to get a list of logs """ - return self._api_request(self.LOGS_ENDPOINT, self.HTTP_GET) + return self._api_request( + self.LOGS_ENDPOINT, + self.HTTP_GET, + timeout=timeout + ) - def get_log(self, log_id): + def get_log(self, log_id, timeout=None): """ API call to get a specific log entry """ - return self._api_request(self.GET_LOG_ENDPOINT % log_id, self.HTTP_GET) + return self._api_request( + self.GET_LOG_ENDPOINT % log_id, + self.HTTP_GET, + timeout=timeout + ) - def get_log_events(self, log_id): + def get_log_events(self, log_id, timeout=None): """ API call to get a specific log entry """ - return self._api_request(self.GET_LOG_EVENTS_ENDPOINT % log_id, self.HTTP_GET) + return self._api_request( + self.GET_LOG_EVENTS_ENDPOINT % log_id, + self.HTTP_GET, + timeout=timeout + ) def emails(self): """ [DEPRECATED] API call to get a list of emails """ return self.templates() - def templates(self): + def templates(self, timeout=None): """ API call to get a list of templates """ - return self._api_request(self.TEMPLATES_ENDPOINT, self.HTTP_GET) + return self._api_request( + self.TEMPLATES_ENDPOINT, + self.HTTP_GET, + timeout=timeout + ) - def get_template(self, template_id, version=None): + def get_template(self, template_id, version=None, timeout=None): """ API call to get a specific template """ if (version): return self._api_request( - self.TEMPLATES_VERSION_ENDPOINT % (template_id, version), self.HTTP_GET) + self.TEMPLATES_VERSION_ENDPOINT % (template_id, version), + self.HTTP_GET, + timeout=timeout + ) else: - return self._api_request(self.TEMPLATES_SPECIFIC_ENDPOINT % template_id, self.HTTP_GET) + return self._api_request( + self.TEMPLATES_SPECIFIC_ENDPOINT % template_id, + self.HTTP_GET, + timeout=timeout + ) def create_email(self, name, subject, html, text=''): """ [DECPRECATED] API call to create an email """ return self.create_template(name, subject, html, text) - def create_template(self, name, subject, html, text=''): + def create_template( + self, + name, + subject, + html, + text='', + timeout=None + ): """ API call to create a template """ payload = { 'name': name, @@ -226,9 +277,20 @@ def create_template(self, name, subject, html, text=''): return self._api_request( self.TEMPLATES_ENDPOINT, self.HTTP_POST, - payload=payload) + payload=payload, + timeout=timeout + ) - def create_new_locale(self, template_id, locale, version_name, subject, text='', html=''): + def create_new_locale( + self, + template_id, + locale, + version_name, + subject, + text='', + html='', + timeout=None + ): """ API call to create a new locale and version of a template """ payload = { 'locale': locale, @@ -244,9 +306,20 @@ def create_new_locale(self, template_id, locale, version_name, subject, text='', return self._api_request( self.TEMPLATES_LOCALES_ENDPOINT % template_id, self.HTTP_POST, - payload=payload) + payload=payload, + timeout=timeout + ) - def create_new_version(self, name, subject, text='', template_id=None, html=None, locale=None): + def create_new_version( + self, + name, + subject, + text='', + template_id=None, + html=None, + locale=None, + timeout=None + ): """ API call to create a new version of a template """ if(html): payload = { @@ -263,16 +336,30 @@ def create_new_version(self, name, subject, text='', template_id=None, html=None } if locale: - url = self.TEMPLATES_SPECIFIC_LOCALE_VERSIONS_ENDPOINT % (template_id, locale) + url = self.TEMPLATES_SPECIFIC_LOCALE_VERSIONS_ENDPOINT % ( + template_id, + locale + ) else: url = self.TEMPLATES_NEW_VERSION_ENDPOINT % template_id return self._api_request( url, self.HTTP_POST, - payload=payload) + payload=payload, + timeout=timeout + ) - def update_template_version(self, name, subject, template_id, version_id, text='', html=None): + def update_template_version( + self, + name, + subject, + template_id, + version_id, + text='', + html=None, + timeout=None + ): """ API call to update a template version """ if(html): payload = { @@ -291,25 +378,40 @@ def update_template_version(self, name, subject, template_id, version_id, text=' return self._api_request( self.TEMPLATES_VERSION_ENDPOINT % (template_id, version_id), self.HTTP_PUT, - payload=payload) + payload=payload, + timeout=timeout + ) - def snippets(self): + def snippets(self, timeout=None): """ API call to get list of snippets """ - return self._api_request(self.SNIPPETS_ENDPOINT, self.HTTP_GET) + return self._api_request( + self.SNIPPETS_ENDPOINT, + self.HTTP_GET, + timeout=timeout + ) - def get_snippet(self, snippet_id): + def get_snippet(self, snippet_id, timeout=None): """ API call to get a specific Snippet """ - return self._api_request(self.SNIPPET_ENDPOINT % (snippet_id), self.HTTP_GET) + return self._api_request( + self.SNIPPET_ENDPOINT % (snippet_id), + self.HTTP_GET, + timeout=timeout + ) - def create_snippet(self, name, body): + def create_snippet(self, name, body, timeout=None): """ API call to create a Snippet """ payload = { 'name': name, 'body': body } - return self._api_request(self.SNIPPETS_ENDPOINT, self.HTTP_POST, payload=payload) + return self._api_request( + self.SNIPPETS_ENDPOINT, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def update_snippet(self, snippet_id, name, body): + def update_snippet(self, snippet_id, name, body, timeout=None): payload = { 'name': name, 'body': body @@ -318,32 +420,55 @@ def update_snippet(self, snippet_id, name, body): return self._api_request( self.SNIPPET_ENDPOINT % (snippet_id), self.HTTP_PUT, - payload=payload + payload=payload, + timeout=timeout ) - def drip_deactivate(self, email_address): + def drip_deactivate(self, email_address, timeout=None): payload = {'email_address': email_address} return self._api_request( self.DRIPS_DEACTIVATE_ENDPOINT, self.HTTP_POST, - payload=payload) + payload=payload, + timeout=timeout + ) + + def _make_file_dict(self, f): + """Make a dictionary with filename and base64 file data""" + if isinstance(f, dict): + file_obj = f['file'] + if 'filename' in f: + file_name = f['filename'] + else: + file_name = file_obj.name + else: + file_obj = f + file_name = f.name + + b64_data = base64.b64encode(file_obj.read()) + return { + 'id': file_name, + 'data': b64_data.decode() if six.PY3 else b64_data, + } def send( - self, - email_id, - recipient, - email_data=None, - sender=None, - cc=None, - bcc=None, - tags=[], - headers={}, - esp_account=None, - locale=None, - email_version_name=None, - inline=None, - files=[]): + self, + email_id, + recipient, + email_data=None, + sender=None, + cc=None, + bcc=None, + tags=[], + headers={}, + esp_account=None, + locale=None, + email_version_name=None, + inline=None, + files=[], + timeout=None + ): """ API call to send an email """ if not email_data: email_data = {} @@ -356,7 +481,7 @@ def send( recipient = {'address': recipient} payload = { - 'email_id': email_id, + 'email_id': email_id, 'recipient': recipient, 'email_data': email_data } @@ -383,18 +508,26 @@ def send( if headers: if not type(headers) == dict: logger.error( - 'kwarg headers must be type(dict), got %s' % (type(headers))) + 'kwarg headers must be type(dict), got %s' % ( + type(headers) + ) + ) payload['headers'] = headers if esp_account: if not isinstance(esp_account, string_types): logger.error( - 'kwarg esp_account must be a string, got %s' % (type(esp_account))) + 'kwarg esp_account must be a string, got %s' % ( + type(esp_account) + ) + ) payload['esp_account'] = esp_account if locale: if not isinstance(locale, string_types): - logger.error('kwarg locale must be a string, got %s' % (type(locale))) + logger.error( + 'kwarg locale must be a string, got %s' % (type(locale)) + ) payload['locale'] = locale if email_version_name: @@ -405,54 +538,19 @@ def send( payload['version_name'] = email_version_name if inline: - if isinstance(inline, file): - image = ({'id': inline.name, 'data': base64.b64encode(inline.read())}) - - payload['inline'] = image - - else: - logger.error( - 'kwarg files must be type(file), got %s' % type(inline)) + payload['inline'] = self._make_file_dict(inline) if files: - file_list = [] - if isinstance(files, list): - for f in files: - file_list.append({'id': f.name, 'data': base64.b64encode(f.read())}) - - payload['files'] = file_list - - else: - logger.error( - 'kwarg files must be type(list), got %s' % type(files)) + payload['files'] = [self._make_file_dict(f) for f in files] return self._api_request( self.SEND_ENDPOINT, self.HTTP_POST, - payload=payload) - - def segments(self): - """ API call to get a list of segments """ - return self._api_request(self.SEGMENTS_ENDPOINT, self.HTTP_GET) - - def run_segment(self, segment_id): - """ API call to run a segment, and return the customers""" - return self._api_request(self.RUN_SEGMENT_ENDPOINT % segment_id, self.HTTP_GET) - - def send_segment(self, email_id, segment_id, email_data=None): - """ API call to send a template, with data, to an entire segment""" - if not email_data: - email_data = {} - - payload = { - 'email_id': email_id, - 'email_data': email_data - } - - return self._api_request(self.SEND_SEGMENT_ENDPOINT % segment_id, - self.HTTP_POST, payload=payload) + payload=payload, + timeout=timeout + ) - def customer_create(self, email, data=None): + def customer_create(self, email, data=None, timeout=None): if not data: data = {} @@ -461,74 +559,51 @@ def customer_create(self, email, data=None): 'data': data } - return self._api_request(self.CUSTOMER_CREATE_ENDPOINT, - self.HTTP_POST, payload=payload) + return self._api_request( + self.CUSTOMER_CREATE_ENDPOINT, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def customer_details(self, email): + def customer_details(self, email, timeout=None): endpoint = self.CUSTOMER_DETAILS_ENDPOINT % email - return self._api_request(endpoint, self.HTTP_GET) + return self._api_request( + endpoint, + self.HTTP_GET, + timeout=timeout + ) - def customer_delete(self, email): + def customer_delete(self, email, timeout=None): endpoint = self.CUSTOMER_DELETE_ENDPOINT % email - return self._api_request(endpoint, self.HTTP_DELETE) - - def customer_conversion(self, email, revenue=None): - endpoint = self.CUSTOMER_CONVERSION_ENDPOINT % email - - payload = { - 'revenue': revenue - } - - return self._api_request(endpoint, self.HTTP_POST, payload=payload) - - def create_customer_group(self, name, description=''): - endpoint = self.GROUPS_ENDPOINT - - payload = { - "name": name, - "description": description - } - return self._api_request(endpoint, self.HTTP_POST, payload=payload) - - def delete_customer_group(self, group_id): - endpoint = self.GROUP_ENDPOINT % group_id - - return self._api_request(endpoint, self.HTTP_DELETE) - - def update_customer_group(self, group_id, name='', description=''): - endpoint = self.GROUP_ENDPOINT % group_id - - payload = { - "name": name, - "description": description - } - - return self._api_request(endpoint, self.HTTP_PUT, payload=payload) - - def add_customer_to_group(self, email, group_id): - endpoint = self.CUSTOMER_GROUPS_ENDPOINT % (email, group_id) - return self._api_request(endpoint, self.HTTP_POST) - - def remove_customer_from_group(self, email, group_id): - endpoint = self.CUSTOMER_GROUPS_ENDPOINT % (email, group_id) - return self._api_request(endpoint, self.HTTP_DELETE) + return self._api_request( + endpoint, + self.HTTP_DELETE, + timeout=timeout + ) - def list_drip_campaigns(self): - return self._api_request(self.DRIP_CAMPAIGN_LIST_ENDPOINT, self.HTTP_GET) + def list_drip_campaigns(self, timeout=None): + return self._api_request( + self.DRIP_CAMPAIGN_LIST_ENDPOINT, + self.HTTP_GET, + timeout=timeout + ) def start_on_drip_campaign( - self, - drip_campaign_id, - recipient, - email_data={}, - sender=None, - cc=None, - bcc=None, - tags=[], - esp_account=None, - locale=None): + self, + drip_campaign_id, + recipient, + email_data={}, + sender=None, + cc=None, + bcc=None, + tags=[], + esp_account=None, + locale=None, + timeout=None + ): endpoint = self.DRIP_CAMPAIGN_ACTIVATE_ENDPOINT % drip_campaign_id payload = { @@ -562,28 +637,52 @@ def start_on_drip_campaign( if esp_account: if not isinstance(esp_account, string_types): logger.error( - 'kwarg esp_account must be a string, got %s' % (type(esp_account))) + 'kwarg esp_account must be a string, got %s' % ( + type(esp_account) + ) + ) payload['esp_account'] = esp_account if locale: if not isinstance(locale, string_types): - logger.error('kwarg locale must be a string, got %s' % (type(locale))) + logger.error( + 'kwarg locale must be a string, got %s' % (type(locale)) + ) payload['locale'] = locale - return self._api_request(endpoint, self.HTTP_POST, payload=payload) + return self._api_request( + endpoint, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def remove_from_drip_campaign(self, recipient_address, drip_campaign_id): + def remove_from_drip_campaign( + self, + recipient_address, + drip_campaign_id, + timeout=None + ): endpoint = self.DRIP_CAMPAIGN_DEACTIVATE_ENDPOINT % drip_campaign_id payload = { 'recipient_address': recipient_address } - return self._api_request(endpoint, self.HTTP_POST, payload=payload) + return self._api_request( + endpoint, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def drip_campaign_details(self, drip_campaign_id): + def drip_campaign_details(self, drip_campaign_id, timeout=None): endpoint = self.DRIP_CAMPAIGN_DETAILS_ENDPOINT % drip_campaign_id - return self._api_request(endpoint, self.HTTP_GET) + return self._api_request( + endpoint, + self.HTTP_GET, + timeout=timeout + ) def start_batch(self): return BatchAPI( @@ -593,15 +692,19 @@ def start_batch(self): API_PORT=self.API_PORT, API_VERSION=self.API_VERSION, DEBUG=self.DEBUG, - json_encoder=self._json_encoder) + json_encoder=self._json_encoder + ) def render( - self, - email_id, - email_data, - version_id=None, - version_name=None, - strict=False): + self, + email_id, + email_data, + version_id=None, + version_name=None, + strict=False, + timeout=None, + locale=None + ): payload = { "template_id": email_id, @@ -617,7 +720,15 @@ def render( if strict: payload['strict'] = strict - return self._api_request(self.RENDER_ENDPOINT, self.HTTP_POST, payload=payload) + if locale: + payload['locale'] = locale + + return self._api_request( + self.RENDER_ENDPOINT, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) class BatchAPI(api): @@ -647,7 +758,7 @@ def _api_request(self, endpoint, http_method, *args, **kwargs): self._commands.append(command) - def execute(self): + def execute(self, timeout=None): """Execute all currently queued batch commands""" logger.debug(' > Batch API request (length %s)' % len(self._commands)) @@ -661,7 +772,13 @@ def execute(self): path = self._build_request_path(self.BATCH_ENDPOINT) data = json.dumps(self._commands, cls=self._json_encoder) - r = requests.post(path, auth=auth, headers=headers, data=data) + r = requests.post( + path, + auth=auth, + headers=headers, + data=data, + timeout=(self.DEFAULT_TIMEOUT if timeout is None else timeout) + ) self._commands = [] diff --git a/sendwithus/test/__init__.py b/sendwithus/test/__init__.py deleted file mode 100644 index 93d9314..0000000 --- a/sendwithus/test/__init__.py +++ /dev/null @@ -1,406 +0,0 @@ -import json -import unittest -import decimal -import time - -from sendwithus import api -from sendwithus.exceptions import APIError, AuthenticationError - - -class TestAPI(unittest.TestCase): - API_KEY = 'THIS_IS_A_TEST_API_KEY' - EMAIL_ID = 'test_fixture_1' - - options = { - 'DEBUG': False - } - - def setUp(self): - self.api = api(self.API_KEY, **self.options) - self.email_address = 'person@example.com' - self.segment_id = 'seg_VC8FDxDno9X64iUPDFSd76' - self.recipient = { - 'name': 'Matt', - 'address': 'us@sendwithus.com'} - self.incomplete_recipient = {'name': 'Matt'} - self.email_data = { - 'name': 'Jimmy', - 'plants': ['Tree', 'Bush', 'Shrub']} - self.email_data_with_decimal = { - 'decimal': decimal.Decimal('5.5') - } - self.sender = { - 'name': 'Company', - 'address': 'company@company.com', - 'reply_to': 'info@company.com'} - self.cc_test = [{ - 'name': 'Matt CC', - 'address': 'test+cc@sendwithus.com'}] - self.bcc_test = [{ - 'name': 'Matt BCC', - 'address': 'test+bcc@sendwithus.com'}] - self.enabled_drip_campaign_id = 'dc_Rmd7y5oUJ3tn86sPJ8ESCk' - self.disabled_drip_campaign_id = 'dc_AjR6Ue9PHPFYmEu2gd8x5V' - self.false_drip_campaign_id = 'false_drip_campaign_id' - self.drip_campaign_step_id = 'dcs_yaAMiZNWCLAEGw7GLjBuGY' - - def assertSuccess(self, result): - self.assertEqual(result.status_code, 200) - try: - self.assertNotEqual(result.json(), None) - except: - self.fail("json() data expected on success") - - def assertSuccessSend(self, result): - self.assertSuccess(result) - self.assertEqual(result.json().get('status'), 'OK') - self.assertTrue(result.json().get('success')) - self.assertNotEqual(result.json().get('receipt_id'), None) - - def assertFail(self, result): - self.assertNotEqual(result.status_code, 200) - # test status is error - - def test_get_emails(self): - """ Test emails endpoint. """ - result = self.api.emails() - self.assertSuccess(result) - - def test_get_template(self): - """ Test template endpoint. """ - result = self.api.get_template("pmaBsiatWCuptZmojWESme") - self.assertSuccess(result) - - def test_get_template_with_version(self): - """ Test template with version endpoint. """ - result = self.api.get_template("pmaBsiatWCuptZmojWESme", version="ver_pYj27c8DTBsWB4MRsoB2MF") - self.assertSuccess(result) - - def test_create_email_success(self): - """ Test create emails endpoint """ - result = self.api.create_email( - 'name', 'subject', '
') - self.assertSuccess(result) - - def test_create_new_version_success(self): - result = self.api.create_new_version( - 'name', 'subject', text="Some stuff", template_id="pmaBsiatWCuptZmojWESme" - ) - self.assertSuccess(result) - - def test_update_template_version(self): - result = self.api.update_template_version( - 'name', 'subject', "pmaBsiatWCuptZmojWESme", "ver_pYj27c8DTBsWB4MRsoB2MF", text="Some more stuff", - ) - self.assertSuccess(result) - - def test_create_email_bad_name(self): - """ Test create emails endpoint empty name""" - result = self.api.create_email( - '', 'subject', '') - self.assertFail(result) - self.assertEqual(result.status_code, 400) - - def test_create_email_bad_subject(self): - """ Test create emails endpoint empty subject""" - result = self.api.create_email( - 'name', '', '') - self.assertFail(result) - self.assertEqual(result.status_code, 400) - - def test_create_email_bad_html(self): - """ Test create emails endpoint invalid html (no longer fails) """ - result = self.api.create_email( - 'name', 'subject', '