From dd5a75beedb76c480516c18466b7278eec2129d0 Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Wed, 24 Feb 2016 10:34:17 +0100 Subject: [PATCH 01/29] Add Python 3 support for sending attachments. --- sendwithus/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index dbaa36e..cac7d60 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -406,7 +406,10 @@ def send( if inline: if isinstance(inline, file): - image = ({'id': inline.name, 'data': base64.b64encode(inline.read())}) + data = base64.b64encode(inline.read()) + if isinstance(data, bytes) and not isinstance(data, string_types): + data = data.decode('latin1') + image = ({'id': inline.name, 'data': data}) payload['inline'] = image @@ -418,7 +421,10 @@ def send( file_list = [] if isinstance(files, list): for f in files: - file_list.append({'id': f.name, 'data': base64.b64encode(f.read())}) + data = base64.b64encode(f.read()) + if isinstance(data, bytes) and not isinstance(data, string_types): + data = data.decode('latin1') + file_list.append({'id': f.name, 'data': data}) payload['files'] = file_list From 4de08d48a64f70646816057a265d2de1b0ff423a Mon Sep 17 00:00:00 2001 From: Christopher Crone Date: Wed, 24 Feb 2016 10:36:35 +0100 Subject: [PATCH 02/29] Only send files that have been opened with mode 'rb'. --- sendwithus/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index cac7d60..256fbfe 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -405,22 +405,23 @@ def send( payload['version_name'] = email_version_name if inline: - if isinstance(inline, file): + if inline.mode == 'rb': data = base64.b64encode(inline.read()) if isinstance(data, bytes) and not isinstance(data, string_types): data = data.decode('latin1') image = ({'id': inline.name, 'data': data}) payload['inline'] = image - else: - logger.error( - 'kwarg files must be type(file), got %s' % type(inline)) + logger.error('file must be opened with mode \'rb\', got: %s' % inline.mode) if files: file_list = [] if isinstance(files, list): for f in files: + if f.mode != 'rb': + logger.error('file must be opened with mode \'rb\', got: %s' % f.mode) + continue data = base64.b64encode(f.read()) if isinstance(data, bytes) and not isinstance(data, string_types): data = data.decode('latin1') From edcf36b331f1d9439d15b33b2f6498bb111b4095 Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Wed, 3 Aug 2016 15:26:14 -0700 Subject: [PATCH 03/29] Create ISSUE_TEMPLATE --- .github/ISSUE_TEMPLATE | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE 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 From 94e04b8f5e495733a51487c11ce95b3f8665a943 Mon Sep 17 00:00:00 2001 From: Regis FLORET Date: Fri, 7 Oct 2016 16:22:11 +0200 Subject: [PATCH 04/29] Add on file content read for Python 3 (keep compat with Python 2.7) --- sendwithus/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index dbaa36e..ec25c63 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -4,16 +4,17 @@ """ import base64 -import logging import json +import logging +import sys +import warnings + import requests 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') @@ -404,9 +405,11 @@ def send( type(email_version_name))) payload['version_name'] = email_version_name + is_py3 = sys.version_info > (3, 0, 0) + if inline: if isinstance(inline, file): - image = ({'id': inline.name, 'data': base64.b64encode(inline.read())}) + image = ({'id': inline.name, 'data': base64.b64encode(inline.read()).decode() if is_py3 else base64.b64encode(inline.read())}) payload['inline'] = image @@ -418,7 +421,7 @@ def send( file_list = [] if isinstance(files, list): for f in files: - file_list.append({'id': f.name, 'data': base64.b64encode(f.read())}) + file_list.append({'id': f.name, 'data': base64.b64encode(f.read()).decode() if is_py3 else base64.b64encode(f.read())}) payload['files'] = file_list From 6b659d524cb8f8e1ae58ebc5b07639c359358aaa Mon Sep 17 00:00:00 2001 From: Regis FLORET Date: Mon, 24 Oct 2016 18:47:22 +0200 Subject: [PATCH 05/29] Add SWU recommendation --- sendwithus/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index ec25c63..839f9eb 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -10,6 +10,7 @@ import warnings import requests +import six from six import string_types from .encoder import SendwithusJSONEncoder @@ -405,11 +406,9 @@ def send( type(email_version_name))) payload['version_name'] = email_version_name - is_py3 = sys.version_info > (3, 0, 0) - if inline: if isinstance(inline, file): - image = ({'id': inline.name, 'data': base64.b64encode(inline.read()).decode() if is_py3 else base64.b64encode(inline.read())}) + image = ({'id': inline.name, 'data': base64.b64encode(inline.read()).decode() if six.PY3 else base64.b64encode(inline.read())}) payload['inline'] = image @@ -421,7 +420,7 @@ def send( file_list = [] if isinstance(files, list): for f in files: - file_list.append({'id': f.name, 'data': base64.b64encode(f.read()).decode() if is_py3 else base64.b64encode(f.read())}) + file_list.append({'id': f.name, 'data': base64.b64encode(f.read()).decode() if six.PY3 else base64.b64encode(f.read())}) payload['files'] = file_list From 7f1737a66e475d3f3dafbe77d365d1eecef77bbd Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 25 Oct 2016 16:54:15 -0700 Subject: [PATCH 06/29] Increment version --- sendwithus/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sendwithus/version.py b/sendwithus/version.py index a3428ff..c642cc8 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '1.8.0' +version = '1.8.1' diff --git a/setup.py b/setup.py index 3bb016d..3b312af 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sendwithus', - version='1.8.0', + version='1.8.1', author='sendwithus', author_email='us@sendwithus.com', packages=find_packages(), From 2c4e81aa4552a374d7459b6e6aec7b10ca034717 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Sat, 3 Dec 2016 19:45:44 +0100 Subject: [PATCH 07/29] Added a `timeout` argument Also added a `default_timeout` argument to `api`. --- .gitignore | 4 + sendwithus/__init__.py | 416 ++++++++++++++++++++++++++++++----------- 2 files changed, 314 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index 7316d36..5538e89 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.pyc *.swo *.swp +*.sublime-* __pycache__ local_settings.py @@ -20,3 +21,6 @@ build/* # testing/tox .tox/ + +# virtualenv +.venv diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index 839f9eb..2f84d7a 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -6,7 +6,6 @@ import base64 import json import logging -import sys import warnings import requests @@ -72,14 +71,16 @@ class api: API_KEY = 'THIS_IS_A_TEST_API_KEY' DEBUG = False + DEFAULT_TIMEOUT = None - def __init__(self, api_key=None, json_encoder=SendwithusJSONEncoder, raise_errors=False, **kwargs): + 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 @@ -160,21 +161,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: @@ -184,39 +191,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, @@ -228,9 +265,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, @@ -246,9 +294,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 = { @@ -272,9 +331,20 @@ def create_new_version(self, name, subject, text='', template_id=None, html=None 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 = { @@ -293,25 +363,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 @@ -320,32 +405,37 @@ 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 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 = {} @@ -358,7 +448,7 @@ def send( recipient = {'address': recipient} payload = { - 'email_id': email_id, + 'email_id': email_id, 'recipient': recipient, 'email_data': email_data } @@ -431,17 +521,33 @@ def send( return self._api_request( self.SEND_ENDPOINT, self.HTTP_POST, - payload=payload) + payload=payload, + timeout=timeout + ) - def segments(self): + def segments(self, timeout=None): """ API call to get a list of segments """ - return self._api_request(self.SEGMENTS_ENDPOINT, self.HTTP_GET) + return self._api_request( + self.SEGMENTS_ENDPOINT, + self.HTTP_GET, + timeout=timeout + ) - def run_segment(self, segment_id): + def run_segment(self, segment_id, timeout=None): """ API call to run a segment, and return the customers""" - return self._api_request(self.RUN_SEGMENT_ENDPOINT % segment_id, self.HTTP_GET) + return self._api_request( + self.RUN_SEGMENT_ENDPOINT % segment_id, + self.HTTP_GET, + timeout=timeout + ) - def send_segment(self, email_id, segment_id, email_data=None): + def send_segment( + self, + email_id, + segment_id, + email_data=None, + timeout=None + ): """ API call to send a template, with data, to an entire segment""" if not email_data: email_data = {} @@ -451,10 +557,14 @@ def send_segment(self, email_id, segment_id, email_data=None): 'email_data': email_data } - return self._api_request(self.SEND_SEGMENT_ENDPOINT % segment_id, - self.HTTP_POST, payload=payload) + return self._api_request( + self.SEND_SEGMENT_ENDPOINT % segment_id, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def customer_create(self, email, data=None): + def customer_create(self, email, data=None, timeout=None): if not data: data = {} @@ -463,43 +573,80 @@ 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) + return self._api_request( + endpoint, + self.HTTP_DELETE, + timeout=timeout + ) - def customer_conversion(self, email, revenue=None): + def customer_conversion(self, email, revenue=None, timeout=None): endpoint = self.CUSTOMER_CONVERSION_ENDPOINT % email payload = { 'revenue': revenue } - return self._api_request(endpoint, self.HTTP_POST, payload=payload) + return self._api_request( + endpoint, + self.HTTP_POST, + payload=payload, + timeout=None + ) - def create_customer_group(self, name, description=''): + def create_customer_group( + self, + name, + description='', + timeout=None + ): endpoint = self.GROUPS_ENDPOINT payload = { "name": name, "description": description } - return self._api_request(endpoint, self.HTTP_POST, payload=payload) + return self._api_request( + endpoint, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) - def delete_customer_group(self, group_id): + def delete_customer_group(self, group_id, timeout=None): endpoint = self.GROUP_ENDPOINT % group_id - return self._api_request(endpoint, self.HTTP_DELETE) + return self._api_request( + endpoint, + self.HTTP_DELETE, + timeout=timeout + ) - def update_customer_group(self, group_id, name='', description=''): + def update_customer_group( + self, + group_id, + name='', + description='', + timeout=None + ): endpoint = self.GROUP_ENDPOINT % group_id payload = { @@ -507,30 +654,54 @@ def update_customer_group(self, group_id, name='', description=''): "description": description } - return self._api_request(endpoint, self.HTTP_PUT, payload=payload) + return self._api_request( + endpoint, + self.HTTP_PUT, + payload=payload, + timeout=timeout + ) - def add_customer_to_group(self, email, group_id): + def add_customer_to_group(self, email, group_id, timeout=None): endpoint = self.CUSTOMER_GROUPS_ENDPOINT % (email, group_id) - return self._api_request(endpoint, self.HTTP_POST) + return self._api_request( + endpoint, + self.HTTP_POST, + timeout=timeout + ) - def remove_customer_from_group(self, email, group_id): + def remove_customer_from_group( + self, + email, + group_id, + timeout=None + ): 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 = { @@ -572,20 +743,39 @@ def start_on_drip_campaign( 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( @@ -595,15 +785,18 @@ 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 + ): payload = { "template_id": email_id, @@ -619,7 +812,12 @@ def render( if strict: payload['strict'] = strict - return self._api_request(self.RENDER_ENDPOINT, self.HTTP_POST, payload=payload) + return self._api_request( + self.RENDER_ENDPOINT, + self.HTTP_POST, + payload=payload, + timeout=timeout + ) class BatchAPI(api): @@ -649,7 +847,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)) @@ -663,7 +861,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 = [] From 4f2b819d5b942e26a9477c6b2725acf2fd8f47fb Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Wed, 7 Dec 2016 19:29:43 +0100 Subject: [PATCH 08/29] Move to pytest based tests and make use of multiple CPUs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From what I’ve seen the average test time is down to about 20% of what the old test suite took to run. --- .gitignore | 4 + .travis.yml | 28 ++- README.md | 4 +- conftest.py | 92 +++++++ requirements.txt | 2 - sendwithus/__init__.py | 149 +++++++++--- sendwithus/test/__init__.py | 406 ------------------------------- setup.py | 6 + test_base.py | 467 ++++++++++++++++++++++++++++++++++++ tox.ini | 16 +- 10 files changed, 726 insertions(+), 448 deletions(-) create mode 100644 conftest.py delete mode 100644 requirements.txt delete mode 100644 sendwithus/test/__init__.py create mode 100644 test_base.py diff --git a/.gitignore b/.gitignore index 7316d36..53b9964 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ local_settings.py # pypi building stuff MANIFEST +.cache/ dist/ dist/* build/ @@ -18,5 +19,8 @@ build/* .idea *.iml +# Sublime Text +*.sublime-* + # testing/tox .tox/ 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..221a86e 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,7 @@ api.render('tem_12345', { "amount": "$12.00" }, 'French-Version', strict=False) 403 ## to run tests - python setup.py test + tox ### Testing multiple python versions This assumes you have [tox](https://testrun.org/tox/latest/) installed and used @@ -411,4 +411,4 @@ Once all the supported python versions are installed simply run: This will run the tests against all the versions specified in `tox.ini`. ### packaging (internal) - python setup.py sdist upload + python setup.py sdist bdist_wheel upload diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..c3f54c1 --- /dev/null +++ b/conftest.py @@ -0,0 +1,92 @@ +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 segment_id(): + return 'seg_VC8FDxDno9X64iUPDFSd76' + + +@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 839f9eb..a683bf3 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -6,7 +6,6 @@ import base64 import json import logging -import sys import warnings import requests @@ -41,7 +40,8 @@ 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' @@ -63,7 +63,8 @@ class api: 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' @@ -73,7 +74,13 @@ class api: DEBUG = False - def __init__(self, api_key=None, json_encoder=SendwithusJSONEncoder, raise_errors=False, **kwargs): + def __init__( + self, + api_key=None, + json_encoder=SendwithusJSONEncoder, + raise_errors=False, + **kwargs + ): """Constructor, expects api key""" if not api_key: @@ -103,7 +110,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, @@ -119,7 +129,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): @@ -128,7 +143,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 @@ -194,7 +211,10 @@ def get_log(self, log_id): def get_log_events(self, log_id): """ 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 + ) def emails(self): """ [DEPRECATED] API call to get a list of emails """ @@ -208,9 +228,13 @@ def get_template(self, template_id, version=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) 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 + ) def create_email(self, name, subject, html, text=''): """ [DECPRECATED] API call to create an email """ @@ -230,7 +254,15 @@ def create_template(self, name, subject, html, text=''): self.HTTP_POST, payload=payload) - 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='' + ): """ API call to create a new locale and version of a template """ payload = { 'locale': locale, @@ -248,7 +280,15 @@ def create_new_locale(self, template_id, locale, version_name, subject, text='', self.HTTP_POST, payload=payload) - 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 + ): """ API call to create a new version of a template """ if(html): payload = { @@ -265,7 +305,10 @@ 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 @@ -274,7 +317,15 @@ def create_new_version(self, name, subject, text='', template_id=None, html=None self.HTTP_POST, payload=payload) - 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 + ): """ API call to update a template version """ if(html): payload = { @@ -301,7 +352,10 @@ def snippets(self): def get_snippet(self, snippet_id): """ 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 + ) def create_snippet(self, name, body): """ API call to create a Snippet """ @@ -309,7 +363,11 @@ def create_snippet(self, name, body): '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 + ) def update_snippet(self, snippet_id, name, body): payload = { @@ -358,7 +416,7 @@ def send( recipient = {'address': recipient} payload = { - 'email_id': email_id, + 'email_id': email_id, 'recipient': recipient, 'email_data': email_data } @@ -385,18 +443,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: @@ -407,8 +473,14 @@ def send( payload['version_name'] = email_version_name if inline: - if isinstance(inline, file): - image = ({'id': inline.name, 'data': base64.b64encode(inline.read()).decode() if six.PY3 else base64.b64encode(inline.read())}) + if isinstance(inline, file): # noqa, until #47 is fixed + image = { + 'id': inline.name, + 'data': ( + base64.b64encode(inline.read()).decode() + if six.PY3 else base64.b64encode(inline.read()) + ) + } payload['inline'] = image @@ -420,7 +492,13 @@ def send( file_list = [] if isinstance(files, list): for f in files: - file_list.append({'id': f.name, 'data': base64.b64encode(f.read()).decode() if six.PY3 else base64.b64encode(f.read())}) + file_list.append({ + 'id': f.name, + 'data': ( + base64.b64encode(f.read()).decode() + if six.PY3 else base64.b64encode(f.read()) + ) + }) payload['files'] = file_list @@ -439,7 +517,10 @@ def segments(self): 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) + 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""" @@ -518,7 +599,10 @@ def remove_customer_from_group(self, email, group_id): return self._api_request(endpoint, self.HTTP_DELETE) def list_drip_campaigns(self): - return self._api_request(self.DRIP_CAMPAIGN_LIST_ENDPOINT, self.HTTP_GET) + return self._api_request( + self.DRIP_CAMPAIGN_LIST_ENDPOINT, + self.HTTP_GET + ) def start_on_drip_campaign( self, @@ -564,12 +648,17 @@ 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) @@ -619,7 +708,11 @@ def render( if strict: payload['strict'] = strict - return self._api_request(self.RENDER_ENDPOINT, self.HTTP_POST, payload=payload) + return self._api_request( + self.RENDER_ENDPOINT, + self.HTTP_POST, + payload=payload + ) class BatchAPI(api): 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', '') - self.assertEqual(result.status_code, 200) - - def test_send(self): - """ Test a send with no sender info. """ - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data) - self.assertSuccess(result) - - def test_send_decimal(self): - """ Test a send with decimal in json """ - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data_with_decimal) - self.assertSuccess(result) - - def test_send_sender_info(self): - """ Test send with sender info. """ - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - sender=self.sender) - self.assertSuccess(result) - - def test_send_cc(self): - """ Test send with cc info. """ - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - cc=self.cc_test) - self.assertSuccess(result) - - def test_send_bcc(self): - """ Test send with bcc info. """ - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - bcc=self.bcc_test) - self.assertSuccess(result) - - def test_send_incomplete(self): - """ Test send with incomplete receiver. """ - result = self.api.send( - self.EMAIL_ID, - self.incomplete_recipient, - email_data=self.email_data) - self.assertFail(result) - - def test_send_invalid_apikey(self): - """ Test send with invalid API key. """ - invalid_api = api('INVALID_API_KEY', **self.options) - result = invalid_api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data) - self.assertFail(result) - self.assertEqual(result.status_code, 403) # bad api key - - def test_send_invalid_email(self): - """ Test send with invalid email_id. """ - result = self.api.send( - 'INVALID_EMAIL_ID', - self.recipient, - email_data=self.email_data) - self.assertFail(result) - self.assertEqual(result.status_code, 400) # invalid email_id - - def test_send_invalid_cc(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - cc='bad') - self.assertFail(result) - - def test_send_invalid_bcc(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - bcc='bad') - self.assertFail(result) - - def test_send_tags(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - tags=['tag_one', 'tag_two', 'tag_three']) - self.assertSuccess(result) - - def test_send_tags_invalid(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - tags='bad') - self.assertFail(result) - - def test_send_headers(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data = self.email_data, - headers={'X-HEADER-ONE': 'header-value'}) - self.assertSuccess(result) - - def test_send_headers_invalid(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data = self.email_data, - headers='X-HEADER-ONE') - self.assertFail(result) - - def test_drip_deactivate(self): - result = self.api.drip_deactivate(self.email_address) - self.assertSuccess(result) - - def test_version_name(self): - result = self.api.send( - self.EMAIL_ID, - self.recipient, - email_data=self.email_data, - email_version_name='version-override') - self.assertSuccess(result) - - def test_customer_actions(self): - data = {'first_name': 'Python Client Unit Test'} - result = self.api.customer_create('test+python@sendwithus.com', data) - self.assertSuccess(result) - result = self.api.customer_delete('test+python@sendwithus.com') - self.assertSuccess(result) - - def test_get_customer(self): - result = self.api.customer_details('customer@example.com') - self.assertSuccess(result) - - def test_customer_conversion(self): - result = self.api.customer_conversion('test+python@sendwithus.com') - self.assertSuccess(result) - - def test_customer_conversion_revenue(self): - result = self.api.customer_conversion('test+python@sendwithus.com', revenue=1234) - self.assertSuccess(result) - - def test_customer_group_actions(self): - result = self.api.create_customer_group(name=str(time.time()), description='sample description') - self.assertSuccess(result) - group_id = json.loads(result.text)['group']['id'] - result = self.api.update_customer_group(group_id=group_id, name='new+name' + str(time.time()), description='new description') - self.assertSuccess(result) - result = self.api.add_customer_to_group(email='customer@example.com', group_id=group_id) - self.assertSuccess(result) - result = self.api.delete_customer_group(group_id=group_id) - self.assertSuccess(result) - - def test_remove_customer_from_group(self): - result = self.api.remove_customer_from_group(email='customer@example.com', group_id='grp_1234') - self.assertSuccess(result) - - def test_send_segment(self): - result = self.api.send_segment(self.EMAIL_ID, self.segment_id) - self.assertSuccess(result) - - def test_list_drip_campaigns(self): - """ Test listing drip campaigns. """ - result = self.api.list_drip_campaigns() - self.assertSuccess(result) - - def test_start_on_drip_campaign(self): - """ Test starting a customer on a drip campaign. """ - result = self.api.start_on_drip_campaign( - self.enabled_drip_campaign_id, - {'address': self.email_address} - ) - self.assertSuccess(result) - - def test_start_on_disabled_drip_campaign(self): - """ Test starting a customer on a drip campaign. """ - result = self.api.start_on_drip_campaign( - self.disabled_drip_campaign_id, - {'address': self.email_address} - ) - self.assertFail(result) - - def test_start_on_false_drip_campaign(self): - """ Test starting a customer on a drip campaign. """ - result = self.api.start_on_drip_campaign( - self.false_drip_campaign_id, - {'address': self.email_address} - ) - self.assertFail(result) - - def test_start_on_drip_campaign_with_data(self): - """ Test starting a customer on a drip campaign with data. """ - result = self.api.start_on_drip_campaign( - self.enabled_drip_campaign_id, - {'address': self.email_address}, - email_data=self.email_data - ) - self.assertSuccess(result) - - def test_remove_from_drip_campaign(self): - """ Test removing a customer from a drip campaign. """ - result = self.api.remove_from_drip_campaign( - self.email_address, - self.enabled_drip_campaign_id) - self.assertSuccess(result) - - def test_drip_campaign_details(self): - """ Test listing drip campaign details. """ - result = self.api.drip_campaign_details(self.enabled_drip_campaign_id) - self.assertSuccess(result) - - def test_batch_create_customer(self): - batch_api_one = self.api.start_batch() - batch_api_two = self.api.start_batch() - - data = {'segment': 'Batch Updated Customer'} - for x in range(10): - batch_api_one.customer_create('test+python+%s@sendwithus.com' % x, data) - self.assertEqual(batch_api_one.command_length(), x + 1) - - if (x % 2) == 0: - batch_api_two.customer_create('test+python+%s+again@sendwithus.com' % x, data) - self.assertEqual(batch_api_two.command_length(), (x/2)+1) - - # Run batch 1 - result = batch_api_one.execute().json() - self.assertEqual(len(result), 10) - for response in result: - self.assertEqual(response['status_code'], 200) - - # Batch one should be empty, batch two still full - self.assertEqual(batch_api_one.command_length(), 0) - self.assertEqual(batch_api_two.command_length(), 5) - - result = batch_api_two.execute().json() - self.assertEqual(len(result), 5) - for response in result: - self.assertEqual(response['status_code'], 200) - - # Both batches now empty - self.assertEqual(batch_api_one.command_length(), 0) - self.assertEqual(batch_api_two.command_length(), 0) - - def test_render(self): - result = self.api.render(self.EMAIL_ID, self.email_data) - self.assertSuccess(result) - - -class TestExceptions(unittest.TestCase): - API_KEY = 'THIS_IS_A_TEST_API_KEY' - - options = { - 'DEBUG': False - } - - def test_authentication_error(self): - """Test raises AuthenticationError with invalid api key""" - invalid_api = api('INVALID_KEY', raise_errors=True, **self.options) - - self.assertRaises(AuthenticationError, invalid_api.emails) - - def test_invalid_request(self): - """Test raises APIError with invalid api request & raise_errors=True""" - swu_api = api(self.API_KEY, raise_errors=True, **self.options) - - self.assertRaises( - APIError, - swu_api.create_email, - 'name', - '', - '' - ) - - def test_raise_errors_option(self): - """Test raises no exception if raise_errors=False""" - swu_api = api(self.API_KEY, raise_errors=False, **self.options) - response = swu_api.create_email('name', '', '') - - self.assertEqual(400, response.status_code) - - -if __name__ == '__main__': - unittest.main() diff --git a/setup.py b/setup.py index 3b312af..94e8114 100755 --- a/setup.py +++ b/setup.py @@ -20,6 +20,12 @@ "requests >= 2.0.0", "six >= 1.9.0" ], + extras_require={ + "test": [ + "pytest >= 3.0.5", + "pytest-xdist >= 1.15.0" + ] + }, classifiers=[ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", diff --git a/test_base.py b/test_base.py new file mode 100644 index 0000000..494fa00 --- /dev/null +++ b/test_base.py @@ -0,0 +1,467 @@ +import json +import decimal +import time + +import pytest +import sendwithus +from sendwithus.exceptions import APIError, AuthenticationError + + +def assert_success(result): + assert result.status_code == 200 + data = result.json() + assert data is not None, data + + +def test_get_emails(api): + """ Test emails endpoint. """ + result = api.emails() + assert_success(result) + + +def test_get_template(api): + """ Test template endpoint. """ + result = api.get_template("pmaBsiatWCuptZmojWESme") + assert_success(result) + + +def test_get_template_with_version(api): + """ Test template with version endpoint. """ + result = api.get_template( + 'pmaBsiatWCuptZmojWESme', + version='ver_pYj27c8DTBsWB4MRsoB2MF' + ) + assert_success(result) + + +def test_create_email_success(api): + """ Test create emails endpoint """ + result = api.create_email( + 'name', + 'subject', + '') + assert_success(result) + + +def test_create_new_version_success(api): + result = api.create_new_version( + 'name', + 'subject', + text="Some stuff", + template_id="pmaBsiatWCuptZmojWESme" + ) + assert_success(result) + + +def test_update_template_version(api): + result = api.update_template_version( + 'name', + 'subject', + 'pmaBsiatWCuptZmojWESme', + 'ver_pYj27c8DTBsWB4MRsoB2MF', + text='Some more stuff', + ) + assert_success(result) + + +def test_create_email_bad_name(api): + """ Test create emails endpoint empty name""" + result = api.create_email( + '', 'subject', '') + assert result.status_code == 400 + + +def test_create_email_bad_subject(api): + """ Test create emails endpoint empty subject""" + result = api.create_email( + 'name', '', '') + assert result.status_code == 400 + + +def test_create_email_bad_html(api): + """ Test create emails endpoint invalid html (no longer fails) """ + result = api.create_email( + 'name', 'subject', '') + assert result.status_code == 200 + + +def test_send(api, email_id, recipient, email_data): + """ Test a send with no sender info. """ + result = api.send(email_id, recipient, email_data=email_data) + assert_success(result) + + +def test_send_decimal(api, email_id, recipient): + """ Test a send with decimal in json """ + result = api.send( + email_id, + recipient, + email_data={ + 'decimal': decimal.Decimal('5.5') + } + ) + assert_success(result) + + +def test_send_sender_info(api, email_id, recipient, email_data, sender): + """ Test send with sender info. """ + result = api.send( + email_id, + recipient, + email_data=email_data, + sender=sender + ) + assert_success(result) + + +def test_send_cc(api, email_id, recipient, email_data, cc_test): + """ Test send with cc info. """ + result = api.send( + email_id, + recipient, + email_data=email_data, + cc=cc_test + ) + assert_success(result) + + +def test_send_bcc(api, email_id, recipient, email_data, bcc_test): + """ Test send with bcc info. """ + result = api.send( + email_id, + recipient, + email_data=email_data, + bcc=bcc_test + ) + assert_success(result) + + +def test_send_incomplete(api, email_id, email_data): + """ Test send with incomplete receiver. """ + result = api.send( + email_id, + {'name': 'Matt'}, + email_data=email_data + ) + assert result.status_code != 200 + + +def test_send_invalid_apikey( + api_options, + email_id, + recipient, + email_data +): + """ Test send with invalid API key. """ + invalid_api = sendwithus.api('INVALID_API_KEY', **api_options) + result = invalid_api.send( + email_id, + recipient, + email_data=email_data + ) + assert result.status_code == 403 # bad api key + + +def test_send_invalid_email(api, recipient, email_data): + """ Test send with invalid email_id. """ + result = api.send( + 'INVALID_EMAIL_ID', + recipient, + email_data=email_data + ) + assert result.status_code == 400 # invalid email_id + + +def test_send_invalid_cc(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + cc='bad' + ) + assert result.status_code != 200 + + +def test_send_invalid_bcc(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + bcc='bad' + ) + assert result.status_code != 200 + + +def test_send_tags(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + tags=['tag_one', 'tag_two', 'tag_three'] + ) + assert_success(result) + + +def test_send_tags_invalid(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + tags='bad' + ) + assert result.status_code != 200 + + +def test_send_headers(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + headers={'X-HEADER-ONE': 'header-value'} + ) + assert_success(result) + + +def test_send_headers_invalid(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + headers='X-HEADER-ONE' + ) + assert result.status_code != 200 + + +def test_drip_deactivate(api, email_address): + result = api.drip_deactivate(email_address) + assert_success(result) + + +def test_version_name(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + email_version_name='version-override' + ) + assert_success(result) + + +def test_customer_actions(api): + data = {'first_name': 'Python Client Unit Test'} + result = api.customer_create('test+python@sendwithus.com', data) + assert_success(result) + result = api.customer_delete('test+python@sendwithus.com') + assert_success(result) + + +def test_get_customer(api): + result = api.customer_details('customer@example.com') + assert_success(result) + + +def test_customer_conversion(api): + result = api.customer_conversion('test+python@sendwithus.com') + assert_success(result) + + +def test_customer_conversion_revenue(api): + result = api.customer_conversion( + 'test+python@sendwithus.com', + revenue=1234 + ) + assert_success(result) + + +def test_customer_group_actions(api): + result = api.create_customer_group( + name=str(time.time()), + description='sample description' + ) + assert_success(result) + group_id = json.loads(result.text)['group']['id'] + result = api.update_customer_group( + group_id=group_id, + name='new+name' + str(time.time()), + description='new description' + ) + assert_success(result) + result = api.add_customer_to_group( + email='customer@example.com', + group_id=group_id + ) + assert_success(result) + result = api.delete_customer_group(group_id=group_id) + assert_success(result) + + +def test_remove_customer_from_group(api): + result = api.remove_customer_from_group( + email='customer@example.com', + group_id='grp_1234' + ) + assert_success(result) + + +def test_send_segment(api, email_id, segment_id): + result = api.send_segment(email_id, segment_id) + assert_success(result) + + +def test_list_drip_campaigns(api): + """ Test listing drip campaigns. """ + result = api.list_drip_campaigns() + assert_success(result) + + +def test_start_on_drip_campaign(api, enabled_drip_campaign_id, email_address): + """ Test starting a customer on a drip campaign. """ + result = api.start_on_drip_campaign( + enabled_drip_campaign_id, + {'address': email_address} + ) + assert_success(result) + + +def test_start_on_disabled_drip_campaign( + api, + disabled_drip_campaign_id, + email_address +): + """ Test starting a customer on a drip campaign. """ + result = api.start_on_drip_campaign( + disabled_drip_campaign_id, + {'address': email_address} + ) + assert result.status_code != 200 + + +def test_start_on_false_drip_campaign(api, email_address): + """ Test starting a customer on a drip campaign. """ + result = api.start_on_drip_campaign( + 'false_drip_campaign_id', + {'address': email_address} + ) + assert result.status_code != 200 + + +def test_start_on_drip_campaign_with_data( + api, + enabled_drip_campaign_id, + email_address, + email_data +): + """ Test starting a customer on a drip campaign with data. """ + result = api.start_on_drip_campaign( + enabled_drip_campaign_id, + {'address': email_address}, + email_data=email_data + ) + assert_success(result) + + +def test_remove_from_drip_campaign( + api, + email_address, + enabled_drip_campaign_id +): + """ Test removing a customer from a drip campaign. """ + result = api.remove_from_drip_campaign( + email_address, + enabled_drip_campaign_id + ) + assert_success(result) + + +def test_drip_campaign_details(api, enabled_drip_campaign_id): + """ Test listing drip campaign details. """ + result = api.drip_campaign_details(enabled_drip_campaign_id) + assert_success(result) + + +def test_batch_create_customer(api): + batch_api_one = api.start_batch() + batch_api_two = api.start_batch() + + data = {'segment': 'Batch Updated Customer'} + for x in range(10): + batch_api_one.customer_create( + 'test+python+%s@sendwithus.com' % x, + data + ) + assert batch_api_one.command_length() == x + 1 + + if (x % 2) == 0: + batch_api_two.customer_create( + 'test+python+%s+again@sendwithus.com' % x, + data + ) + assert batch_api_two.command_length() == x / 2 + 1 + + # Run batch 1 + result = batch_api_one.execute().json() + assert len(result) == 10 + for response in result: + assert response['status_code'] == 200 + + # Batch one should be empty, batch two still full + assert batch_api_one.command_length() == 0 + assert batch_api_two.command_length() == 5 + + result = batch_api_two.execute().json() + assert len(result) == 5 + for response in result: + assert response['status_code'] == 200 + + # Both batches now empty + assert batch_api_one.command_length() == 0 + assert batch_api_two.command_length() == 0 + + +def test_render(api, email_id, email_data): + result = api.render(email_id, email_data) + assert_success(result) + + +def test_authentication_error(api_options): + """Test raises AuthenticationError with invalid api key""" + invalid_api = sendwithus.api( + 'INVALID_KEY', + raise_errors=True, + **api_options + ) + + with pytest.raises(AuthenticationError): + invalid_api.emails() + + +def test_invalid_request(api_key, api_options): + """Test raises APIError with invalid api request & raise_errors=True""" + swu_api = sendwithus.api( + api_key, + raise_errors=True, + **api_options + ) + + with pytest.raises(APIError): + swu_api.create_email( + 'name', + '', + '' + ) + + +def test_raise_errors_option(api_key, api_options): + """Test raises no exception if raise_errors=False""" + swu_api = sendwithus.api( + api_key, + raise_errors=False, + **api_options + ) + response = swu_api.create_email( + 'name', + '', + '' + ) + + assert 400 == response.status_code diff --git a/tox.ini b/tox.ini index 87c4e4c..2610461 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,17 @@ [tox] -envlist = py26, py27, py34, py35 +envlist = py{26,27,34,35}, lint skip_missing_interpreters = true [testenv] -deps = -r{toxinidir}/requirements.txt -commands = python setup.py test +deps = .[test] +commands = py.test -n auto + +[testenv:lint] +commands = + flake8 sendwithus/ test_base.py + isort --verbose --recursive --diff sendwithus/ + isort --verbose --recursive --check-only sendwithus/ +deps = + . + flake8>=3.2.1 + isort>=4.2.5 From 6893153716778f8f5630275ee385363f7f9735eb Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Tue, 3 Jan 2017 12:40:12 -0800 Subject: [PATCH 09/29] Update .gitignore Remove `*.sublime-*` --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5538e89..32e651b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ *.pyc *.swo *.swp -*.sublime-* __pycache__ local_settings.py From 71d85b1fed37e36beb52ded999ab0de907b02588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Benesch?= Date: Thu, 5 Jan 2017 11:48:34 +0100 Subject: [PATCH 10/29] Allow upload with provided filenames --- README.md | 12 ++++++++++++ sendwithus/__init__.py | 18 +++++++++++------- test_base.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 221a86e..21bea11 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,18 @@ print r.status_code # 200 ``` +### Optional File Attachments With explicit file names + +```python +r = api.send( + email_id='YOUR-EMAIL-ID', + recipient={'name': 'Matt', + 'address': 'us@sendwithus.com'}, + files=[(open('/home/Matt/report1.txt', 'r'), 'arbitrary_file_name.xyz')]) +print r.status_code +# 200 +``` + ### Optional Inline Image ```python diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index d181eab..a7c95c4 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -546,13 +546,17 @@ def send( file_list = [] if isinstance(files, list): for f in files: - file_list.append({ - 'id': f.name, - 'data': ( - base64.b64encode(f.read()).decode() - if six.PY3 else base64.b64encode(f.read()) - ) - }) + if isinstance(f, tuple): + file_obj, file_name = f + else: + file_obj = f + file_name = f.name + + file_list.append( + {'id': file_name, + 'data': base64.b64encode(file_obj.read()).decode() + if six.PY3 else base64.b64encode(file_obj.read())} + ) payload['files'] = file_list diff --git a/test_base.py b/test_base.py index 494fa00..c8f10e6 100644 --- a/test_base.py +++ b/test_base.py @@ -1,8 +1,11 @@ import json import decimal +import tempfile import time import pytest +import six + import sendwithus from sendwithus.exceptions import APIError, AuthenticationError @@ -232,6 +235,37 @@ def test_send_headers_invalid(api, email_id, recipient, email_data): assert result.status_code != 200 +@pytest.yield_fixture +def file(): + with tempfile.NamedTemporaryFile() as tempf: + data = ('simple file content' + '\n') * 3 + tempf.write(bytes(data, 'utf-8')) \ + if six.PY3 else tempf.write(data) + tempf.seek(0) + yield tempf + + +def test_send_with_files(api, email_id, recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + files=[file]) + + assert result.status_code == 200 + + +def test_send_with_files_explicit_filename(api, email_id, + recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + files=[(file, 'filename.pdf')]) + + assert result.status_code == 200 + + def test_drip_deactivate(api, email_address): result = api.drip_deactivate(email_address) assert_success(result) From 6a7fa76f55267af34df4f1e88b75f45975a00448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Benesch?= Date: Tue, 10 Jan 2017 10:42:44 +0100 Subject: [PATCH 11/29] Review comments --- README.md | 3 ++- sendwithus/__init__.py | 8 ++++++-- test_base.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 21bea11..c1705b7 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,8 @@ r = api.send( email_id='YOUR-EMAIL-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, - files=[(open('/home/Matt/report1.txt', 'r'), 'arbitrary_file_name.xyz')]) + files=[{'file': open('/home/Matt/report1.txt', 'r'), + 'filename': 'arbitrary_file_name.xyz'}]) print r.status_code # 200 ``` diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index a7c95c4..32952a1 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -546,8 +546,12 @@ def send( file_list = [] if isinstance(files, list): for f in files: - if isinstance(f, tuple): - file_obj, file_name = f + 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 diff --git a/test_base.py b/test_base.py index c8f10e6..09d808c 100644 --- a/test_base.py +++ b/test_base.py @@ -245,7 +245,7 @@ def file(): yield tempf -def test_send_with_files(api, email_id, recipient, email_data, file): +def test_send_with_files_valid(api, email_id, recipient, email_data, file): result = api.send( email_id, recipient, @@ -261,11 +261,36 @@ def test_send_with_files_explicit_filename(api, email_id, email_id, recipient, email_data=email_data, - files=[(file, 'filename.pdf')]) + files=[{'file': file, + 'filename': 'filename.pdf'}] + ) assert result.status_code == 200 +def test_send_with_files_valid_1(api, email_id, + recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + files=[{'file': file}] + ) + + assert result.status_code == 200 + + +def test_send_with_files_invalid(api, email_id, + recipient, email_data, file): + with pytest.raises(KeyError): + api.send( + email_id, + recipient, + email_data=email_data, + files=[{'filename': 'filename.pdf'}] + ) + + def test_drip_deactivate(api, email_address): result = api.drip_deactivate(email_address) assert_success(result) From c1a7c2e02ad182c1ea384b8dcc8b4eef0c532434 Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Thu, 12 Jan 2017 15:21:31 -0800 Subject: [PATCH 12/29] Update Version --- sendwithus/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sendwithus/version.py b/sendwithus/version.py index c642cc8..bb58ec9 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '1.8.1' +version = '1.9.0' diff --git a/setup.py b/setup.py index 94e8114..999c5fb 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sendwithus', - version='1.8.1', + version='1.9.0', author='sendwithus', author_email='us@sendwithus.com', packages=find_packages(), From c27ecf616701b68f96c6900ec6e063eff4725f65 Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Thu, 12 Jan 2017 15:39:52 -0800 Subject: [PATCH 13/29] Remove editor specific files from .gitignore --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index 1781e88..ad4b0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -15,13 +15,6 @@ build/ build/* *.egg-info -# IntelliJ -.idea -*.iml - -# Sublime Text -*.sublime-* - # testing/tox .tox/ From d7121f3776a4b5864ada91d34a1fd1dcfd6e65fa Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Mon, 16 Jan 2017 13:20:00 +0100 Subject: [PATCH 14/29] Allow an explicit id value to be configured for the inline argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the same syntax as for passed in objects to the `files` argument. It should be noted that when passing in an invalid inline/files argument it no longer logs out to terminal but tries to read the argument, which will fail in most cases. This is more in line with PEP-20’s “Errors should never pass silently”. --- sendwithus/__init__.py | 58 +++++++++++++------------------------- test_base.py | 64 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index 32952a1..47ee830 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -441,6 +441,24 @@ def drip_deactivate(self, email_address, timeout=None): 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, @@ -527,46 +545,10 @@ def send( payload['version_name'] = email_version_name if inline: - if isinstance(inline, file): # noqa, until #47 is fixed - image = { - 'id': inline.name, - 'data': ( - base64.b64encode(inline.read()).decode() - if six.PY3 else 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: - 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 - - file_list.append( - {'id': file_name, - 'data': base64.b64encode(file_obj.read()).decode() - if six.PY3 else base64.b64encode(file_obj.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, diff --git a/test_base.py b/test_base.py index 09d808c..6f0d50e 100644 --- a/test_base.py +++ b/test_base.py @@ -255,6 +255,16 @@ def test_send_with_files_valid(api, email_id, recipient, email_data, file): assert result.status_code == 200 +def test_send_with_inline_valid(api, email_id, recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + inline=file) + + assert result.status_code == 200 + + def test_send_with_files_explicit_filename(api, email_id, recipient, email_data, file): result = api.send( @@ -268,6 +278,18 @@ def test_send_with_files_explicit_filename(api, email_id, assert result.status_code == 200 +def test_send_with_inline_explicit_filename(api, email_id, + recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + inline={'file': file, 'filename': 'filename.pdf'} + ) + + assert result.status_code == 200 + + def test_send_with_files_valid_1(api, email_id, recipient, email_data, file): result = api.send( @@ -280,6 +302,17 @@ def test_send_with_files_valid_1(api, email_id, assert result.status_code == 200 +def test_send_with_inline_valid_1(api, email_id, + recipient, email_data, file): + result = api.send( + email_id, + recipient, + email_data=email_data, + inline={'file': file}, + ) + assert result.status_code == 200 + + def test_send_with_files_invalid(api, email_id, recipient, email_data, file): with pytest.raises(KeyError): @@ -291,6 +324,37 @@ def test_send_with_files_invalid(api, email_id, ) +def test_send_with_inline_invalid(api, email_id, + recipient, email_data, file): + with pytest.raises(KeyError): + api.send( + email_id, + recipient, + email_data=email_data, + inline={'filename': 'filename.pdf'} + ) + + +def test_send_with_files_invalid_arg(api, email_id, recipient, email_data): + with pytest.raises(AttributeError): + api.send( + email_id, + recipient, + email_data=email_data, + files='1337' + ) + + +def test_send_with_inline_invalid_arg(api, email_id, recipient, email_data): + with pytest.raises(AttributeError): + api.send( + email_id, + recipient, + email_data=email_data, + inline='1337' + ) + + def test_drip_deactivate(api, email_address): result = api.drip_deactivate(email_address) assert_success(result) From 040cba5756475045e2cf819af7e9dd8da6cae336 Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Wed, 18 Jan 2017 10:22:07 -0800 Subject: [PATCH 15/29] Update README and version --- README.md | 40 +++++++++++++++++++++++++++------------- sendwithus/version.py | 2 +- setup.py | 2 +- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c1705b7..2e9dbe9 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ 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,11 @@ print r.status_code # 200 ``` -### Optional File Attachments With explicit file names +### Optional File Attachments with explicit file names ```python r = api.send( - email_id='YOUR-EMAIL-ID', + email_id='YOUR-TEMPLATE-ID', recipient={'name': 'Matt', 'address': 'us@sendwithus.com'}, files=[{'file': open('/home/Matt/report1.txt', 'r'), @@ -208,7 +208,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'}, inline=open('image.jpg', 'r')) @@ -216,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') diff --git a/sendwithus/version.py b/sendwithus/version.py index bb58ec9..b966453 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '1.9.0' +version = '1.10.0' diff --git a/setup.py b/setup.py index 999c5fb..a4b8b50 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sendwithus', - version='1.9.0', + version='1.10.0', author='sendwithus', author_email='us@sendwithus.com', packages=find_packages(), From cf818473ff71a2069d81834592395ae218005ec1 Mon Sep 17 00:00:00 2001 From: Brandon Brown Date: Mon, 23 Jan 2017 13:09:49 -0800 Subject: [PATCH 16/29] Update README Update README to reflect Segmentation API changes as the feature will soon be deprecated from all clients. --- README.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/README.md b/README.md index 2e9dbe9..91ff2c1 100644 --- a/README.md +++ b/README.md @@ -367,27 +367,6 @@ api.delete_customer_group('grp_1234') api.update_customer_group('new_name', 'updated group description') ``` -# Segmentation - -## Send Template to Segment - -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. - -```python -api.send_segment('tem_12345', 'seg_1245') -``` - -### Extra Data - -You may specify extra data to be merged into the template, alongside the -individual customer profiles - -```python -api.send_segment('tem_12345', 'seg_12345', email_data={'color': 'blue'}) -``` - # Render ## Render a Template with data From 3124ebf019fe92c5e459b222434f6521ae4e5362 Mon Sep 17 00:00:00 2001 From: Brandon Brown Date: Mon, 23 Jan 2017 14:31:16 -0800 Subject: [PATCH 17/29] Remove Segmentation support from client API --- conftest.py | 5 ----- sendwithus/__init__.py | 42 ------------------------------------------ test_base.py | 5 ----- 3 files changed, 52 deletions(-) diff --git a/conftest.py b/conftest.py index c3f54c1..1490513 100644 --- a/conftest.py +++ b/conftest.py @@ -27,11 +27,6 @@ def email_address(): return 'person@example.com' -@pytest.fixture -def segment_id(): - return 'seg_VC8FDxDno9X64iUPDFSd76' - - @pytest.fixture def enabled_drip_campaign_id(): return 'dc_Rmd7y5oUJ3tn86sPJ8ESCk' diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index 47ee830..a2fe7ae 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -47,9 +47,6 @@ class api: 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' @@ -557,45 +554,6 @@ def send( timeout=timeout ) - def segments(self, timeout=None): - """ API call to get a list of segments """ - return self._api_request( - self.SEGMENTS_ENDPOINT, - self.HTTP_GET, - timeout=timeout - ) - - def run_segment(self, segment_id, timeout=None): - """ API call to run a segment, and return the customers""" - return self._api_request( - self.RUN_SEGMENT_ENDPOINT % segment_id, - self.HTTP_GET, - timeout=timeout - ) - - def send_segment( - self, - email_id, - segment_id, - email_data=None, - timeout=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, - timeout=timeout - ) - def customer_create(self, email, data=None, timeout=None): if not data: data = {} diff --git a/test_base.py b/test_base.py index 6f0d50e..7e9abd8 100644 --- a/test_base.py +++ b/test_base.py @@ -426,11 +426,6 @@ def test_remove_customer_from_group(api): assert_success(result) -def test_send_segment(api, email_id, segment_id): - result = api.send_segment(email_id, segment_id) - assert_success(result) - - def test_list_drip_campaigns(api): """ Test listing drip campaigns. """ result = api.list_drip_campaigns() From 21815c5908bb8e15410c89d1466a784c29805a30 Mon Sep 17 00:00:00 2001 From: Brandon Brown Date: Wed, 25 Jan 2017 11:51:58 -0800 Subject: [PATCH 18/29] Bump version --- sendwithus/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sendwithus/version.py b/sendwithus/version.py index b966453..37962c7 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '1.10.0' +version = '2.0.0' diff --git a/setup.py b/setup.py index a4b8b50..e9835ea 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sendwithus', - version='1.10.0', + version='2.0.0', author='sendwithus', author_email='us@sendwithus.com', packages=find_packages(), From 67dca027fe4f6244e4b90892236aa72525e12e64 Mon Sep 17 00:00:00 2001 From: Phil Date: Mon, 20 Feb 2017 15:42:34 -0800 Subject: [PATCH 19/29] Add PR Template and Update README --- .github/PULL_REQUEST_TEMPLATE | 28 ++++++ README.md | 160 ++++++++++++++++++++++------------ 2 files changed, 131 insertions(+), 57 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE 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/README.md b/README.md index 91ff2c1..443efe8 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ sendwithus python-client [![Build Status](https://travis-ci.org/sendwithus/sendwithus_python.png)](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,22 +57,22 @@ 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! @@ -243,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 @@ -251,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 @@ -259,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( @@ -276,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 @@ -284,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 @@ -292,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') @@ -300,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. @@ -316,19 +318,19 @@ 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 +### Add Customer to a Group ```python api.add_customer_to_group('customer@example.com', 'grp_1234') ``` -## Remove Customer from a Group +### Remove Customer from a Group ```python api.remove_customer_from_group('customer@example.com', 'grp_1234') @@ -336,7 +338,7 @@ api.remove_customer_from_group('customer@example.com', 'grp_1234') # Conversions -## Create a customer conversion event +### Create a Customer Conversion event You can use the Conversion API to track conversion and revenue data events against your sent emails. @@ -349,19 +351,19 @@ api.customer_conversion('customer@example.com', revenue=10050) # Customer Groups -## Create a Customer Group +### Create a Customer Group ```python api.create_customer_group('group_name', 'sample group description') ``` -## Delete a customer group +### Delete a Customer Group ```python api.delete_customer_group('grp_1234') ``` -## Update a Customer Group +### Update a Customer Group ```python api.update_customer_group('new_name', 'updated group description') @@ -369,17 +371,19 @@ api.update_customer_group('new_name', 'updated group description') # Render -## Render a Template with data +### Render a Template with data -The render API allows you to render a template with data, using the exact same rendering workflow that Sendwithus uses when delivering your email. +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.render('tem_12345', { "amount": "$12.00" }, 'French-Version', strict=False) ``` -## expected response +### Expected Response -### Success +#### Success +```bash >>> r.status_code 200 @@ -391,30 +395,72 @@ api.render('tem_12345', { "amount": "$12.00" }, 'French-Version', strict=False) >>> r.json().get('receipt_id') u'numeric-receipt-id' +``` -### Error cases +#### Error cases * malformed request - - >>> r.status_code - 400 +```bash + >>> r.status_code + 400 +``` * bad API key +```bash + >>> r.status_code + 403 +``` - >>> r.status_code - 403 +## Run Tests +Use [tox](https://tox.readthedocs.io/en/latest/) to run the tests: -## to run tests - tox +```bash +tox +``` -### Testing multiple python versions -This assumes you have [tox](https://testrun.org/tox/latest/) installed and used +### 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. Once all the supported python versions are installed simply run: - tox +```bash +tox +``` This will run the tests against all the versions specified in `tox.ini`. -### packaging (internal) - python setup.py sdist bdist_wheel upload +## Troubleshooting + +### General Troubleshooting + +- 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 + +### Enable Debug Mode + +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 +import sendwithus +api = sendwithus.api(api_key='YOUR-API-KEY', DEBUG=True) +``` +### Response Ranges + +Sendwithus' API typically sends responses back in these ranges: + +- 2xx – Successful Request +- 4xx – Failed Request (Client error) +- 5xx – Failed Request (Server error) + +If you're receiving an error in the 400 response range follow these steps: + +- 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 + +### Internal +To package +```bash + python setup.py sdist bdist_wheel upload +``` From ab753daf7676c02e0f014d5d5be78002e81806ef Mon Sep 17 00:00:00 2001 From: Brandon Brown Date: Thu, 9 Mar 2017 17:35:10 -0800 Subject: [PATCH 20/29] Deprecate support for Customer Groups (#60) * Remove Customer Groups examples from README * Remove Customer Groups API from client --- README.md | 30 ----------------- sendwithus/__init__.py | 73 ------------------------------------------ test_base.py | 30 ----------------- 3 files changed, 133 deletions(-) diff --git a/README.md b/README.md index 443efe8..142001f 100644 --- a/README.md +++ b/README.md @@ -324,17 +324,6 @@ api.customer_create('customer@example.com', data={'first_name': 'Matt'}) api.customer_delete('customer@example.com') ``` -### Add Customer to a Group - -```python -api.add_customer_to_group('customer@example.com', 'grp_1234') -``` - -### Remove Customer from a Group - -```python -api.remove_customer_from_group('customer@example.com', 'grp_1234') -``` # Conversions @@ -349,25 +338,6 @@ against your sent emails. api.customer_conversion('customer@example.com', revenue=10050) ``` -# Customer Groups - -### Create a Customer Group - -```python -api.create_customer_group('group_name', 'sample group description') -``` - -### Delete a Customer Group - -```python -api.delete_customer_group('grp_1234') -``` - -### Update a Customer Group - -```python -api.update_customer_group('new_name', 'updated group description') -``` # Render diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index a2fe7ae..a2b0e8c 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -52,9 +52,6 @@ class api: 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' @@ -602,76 +599,6 @@ def customer_conversion(self, email, revenue=None, timeout=None): timeout=None ) - def create_customer_group( - self, - name, - description='', - timeout=None - ): - endpoint = self.GROUPS_ENDPOINT - - payload = { - "name": name, - "description": description - } - return self._api_request( - endpoint, - self.HTTP_POST, - payload=payload, - timeout=timeout - ) - - def delete_customer_group(self, group_id, timeout=None): - endpoint = self.GROUP_ENDPOINT % group_id - - return self._api_request( - endpoint, - self.HTTP_DELETE, - timeout=timeout - ) - - def update_customer_group( - self, - group_id, - name='', - description='', - timeout=None - ): - endpoint = self.GROUP_ENDPOINT % group_id - - payload = { - "name": name, - "description": description - } - - return self._api_request( - endpoint, - self.HTTP_PUT, - payload=payload, - timeout=timeout - ) - - def add_customer_to_group(self, email, group_id, timeout=None): - endpoint = self.CUSTOMER_GROUPS_ENDPOINT % (email, group_id) - return self._api_request( - endpoint, - self.HTTP_POST, - timeout=timeout - ) - - def remove_customer_from_group( - self, - email, - group_id, - timeout=None - ): - endpoint = self.CUSTOMER_GROUPS_ENDPOINT % (email, group_id) - return self._api_request( - endpoint, - self.HTTP_DELETE, - timeout=timeout - ) - def list_drip_campaigns(self, timeout=None): return self._api_request( self.DRIP_CAMPAIGN_LIST_ENDPOINT, diff --git a/test_base.py b/test_base.py index 7e9abd8..cc4d708 100644 --- a/test_base.py +++ b/test_base.py @@ -396,36 +396,6 @@ def test_customer_conversion_revenue(api): assert_success(result) -def test_customer_group_actions(api): - result = api.create_customer_group( - name=str(time.time()), - description='sample description' - ) - assert_success(result) - group_id = json.loads(result.text)['group']['id'] - result = api.update_customer_group( - group_id=group_id, - name='new+name' + str(time.time()), - description='new description' - ) - assert_success(result) - result = api.add_customer_to_group( - email='customer@example.com', - group_id=group_id - ) - assert_success(result) - result = api.delete_customer_group(group_id=group_id) - assert_success(result) - - -def test_remove_customer_from_group(api): - result = api.remove_customer_from_group( - email='customer@example.com', - group_id='grp_1234' - ) - assert_success(result) - - def test_list_drip_campaigns(api): """ Test listing drip campaigns. """ result = api.list_drip_campaigns() From b1115e45bf9ce6b658acb8b95741b19774c472e2 Mon Sep 17 00:00:00 2001 From: Brandon Brown Date: Thu, 9 Mar 2017 17:39:28 -0800 Subject: [PATCH 21/29] Bump version --- sendwithus/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sendwithus/version.py b/sendwithus/version.py index 37962c7..5918abd 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '2.0.0' +version = '3.0.0' From f6a1ad265a568b7425c760999893a8ca60e7aa5c Mon Sep 17 00:00:00 2001 From: Marie Starck Date: Thu, 30 Mar 2017 16:23:35 +0200 Subject: [PATCH 22/29] Deprecate conversion --- README.md | 14 -------------- sendwithus/__init__.py | 15 --------------- sendwithus/version.py | 2 +- test_base.py | 13 ------------- 4 files changed, 1 insertion(+), 43 deletions(-) diff --git a/README.md b/README.md index 142001f..a4e1c34 100644 --- a/README.md +++ b/README.md @@ -325,20 +325,6 @@ api.customer_delete('customer@example.com') ``` -# Conversions - -### Create a Customer Conversion event - -You can use the Conversion API to track conversion and revenue data events -against your sent emails. - -**NOTE:** Revenue is in cents (eg. $100.50 = 10050) - -```python -api.customer_conversion('customer@example.com', revenue=10050) -``` - - # Render ### Render a Template with data diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index a2b0e8c..b23582b 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -51,7 +51,6 @@ class api: CUSTOMER_CREATE_ENDPOINT = 'customers' CUSTOMER_DETAILS_ENDPOINT = 'customers/%s' CUSTOMER_DELETE_ENDPOINT = 'customers/%s' - CUSTOMER_CONVERSION_ENDPOINT = 'customers/%s/conversions' DRIP_CAMPAIGN_LIST_ENDPOINT = 'drip_campaigns' DRIP_CAMPAIGN_ACTIVATE_ENDPOINT = 'drip_campaigns/%s/activate' DRIP_CAMPAIGN_DEACTIVATE_ENDPOINT = 'drip_campaigns/%s/deactivate' @@ -585,20 +584,6 @@ def customer_delete(self, email, timeout=None): timeout=timeout ) - def customer_conversion(self, email, revenue=None, timeout=None): - endpoint = self.CUSTOMER_CONVERSION_ENDPOINT % email - - payload = { - 'revenue': revenue - } - - return self._api_request( - endpoint, - self.HTTP_POST, - payload=payload, - timeout=None - ) - def list_drip_campaigns(self, timeout=None): return self._api_request( self.DRIP_CAMPAIGN_LIST_ENDPOINT, diff --git a/sendwithus/version.py b/sendwithus/version.py index 5918abd..7893f24 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '3.0.0' +version = '4.0.0' diff --git a/test_base.py b/test_base.py index cc4d708..d2d8084 100644 --- a/test_base.py +++ b/test_base.py @@ -383,19 +383,6 @@ def test_get_customer(api): assert_success(result) -def test_customer_conversion(api): - result = api.customer_conversion('test+python@sendwithus.com') - assert_success(result) - - -def test_customer_conversion_revenue(api): - result = api.customer_conversion( - 'test+python@sendwithus.com', - revenue=1234 - ) - assert_success(result) - - def test_list_drip_campaigns(api): """ Test listing drip campaigns. """ result = api.list_drip_campaigns() From 60e0e7c988f0cf37b9e831d19f2fb0b7330dbfd3 Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Fri, 31 Mar 2017 16:34:52 -0700 Subject: [PATCH 23/29] Remove space --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2610461..2a13da9 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands = py.test -n auto [testenv:lint] commands = - flake8 sendwithus/ test_base.py + flake8 sendwithus/test_base.py isort --verbose --recursive --diff sendwithus/ isort --verbose --recursive --check-only sendwithus/ deps = From 0a3b8177afc498854caf48dceb20b465239a8630 Mon Sep 17 00:00:00 2001 From: Phil Ma Date: Thu, 4 May 2017 16:58:46 -0700 Subject: [PATCH 24/29] Update README.md Fixed typo on optional inline image --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4e1c34..e4f44fd 100644 --- a/README.md +++ b/README.md @@ -223,8 +223,8 @@ 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'}]) + inline={'file': open('/home/Matt/image.jpg, 'r'), + 'filename': 'cool_image.jpg'}) print r.status_code # 200 ``` From 6357a78046b06f48ec6c0736a127894f5cd66eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ml=C4=8Doch?= Date: Mon, 22 May 2017 17:26:52 -0400 Subject: [PATCH 25/29] Adds locale param to render function --- sendwithus/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index b23582b..0cdcc21 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -699,6 +699,7 @@ def render( self, email_id, email_data, + locale=None, version_id=None, version_name=None, strict=False, @@ -710,6 +711,9 @@ def render( "template_data": email_data } + if locale: + payload['locale'] = locale + if version_id: payload['version_id'] = version_id From 9043f89297335bc051874a385703248293915215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ml=C4=8Doch?= Date: Tue, 23 May 2017 10:34:12 -0400 Subject: [PATCH 26/29] Adds test --- test_base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test_base.py b/test_base.py index d2d8084..8dd16ee 100644 --- a/test_base.py +++ b/test_base.py @@ -370,6 +370,16 @@ def test_version_name(api, email_id, recipient, email_data): assert_success(result) +def test_locale(api, email_id, recipient, email_data): + result = api.send( + email_id, + recipient, + email_data=email_data, + locale='sv-SE' + ) + assert_success(result) + + def test_customer_actions(api): data = {'first_name': 'Python Client Unit Test'} result = api.customer_create('test+python@sendwithus.com', data) From be05384ca385910c16186a5c17ec0e627681c897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ml=C4=8Doch?= Date: Tue, 23 May 2017 10:45:49 -0400 Subject: [PATCH 27/29] Updates documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4f44fd..7ec8d8c 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ The Render API allows you to render a template with data, using the exact same r `Strict` is set to `False` as a default, if `Strict=True` this API call will fail on any missing `email_data`. ```python -api.render('tem_12345', { "amount": "$12.00" }, 'French-Version', strict=False) +api.render('tem_12345', { "amount": "$12.00" }, locale='fr-FR', version_name='French-Version', strict=False) ``` ### Expected Response From f62c680a23721f5142f6f08fb0e52c42139bd718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ml=C4=8Doch?= Date: Tue, 23 May 2017 11:36:18 -0400 Subject: [PATCH 28/29] Moves locale param to be last to avoid any backwards compatibility issues with unnamed params --- sendwithus/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sendwithus/__init__.py b/sendwithus/__init__.py index 0cdcc21..7c06125 100644 --- a/sendwithus/__init__.py +++ b/sendwithus/__init__.py @@ -699,11 +699,11 @@ def render( self, email_id, email_data, - locale=None, version_id=None, version_name=None, strict=False, - timeout=None + timeout=None, + locale=None ): payload = { @@ -711,9 +711,6 @@ def render( "template_data": email_data } - if locale: - payload['locale'] = locale - if version_id: payload['version_id'] = version_id @@ -723,6 +720,9 @@ def render( if strict: payload['strict'] = strict + if locale: + payload['locale'] = locale + return self._api_request( self.RENDER_ENDPOINT, self.HTTP_POST, From 4f592d4c9993bfaf7a5750e44441ce3ee34ce5bf Mon Sep 17 00:00:00 2001 From: Dylan Moore Date: Tue, 23 May 2017 14:33:11 -0700 Subject: [PATCH 29/29] Increment version for new locale parameter in render call --- sendwithus/version.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sendwithus/version.py b/sendwithus/version.py index 7893f24..7a4dc61 100644 --- a/sendwithus/version.py +++ b/sendwithus/version.py @@ -1 +1 @@ -version = '4.0.0' +version = '4.1.0' diff --git a/setup.py b/setup.py index e9835ea..6b88095 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='sendwithus', - version='2.0.0', + version='4.1.0', author='sendwithus', author_email='us@sendwithus.com', packages=find_packages(),