diff --git a/docs/index.rst b/docs/index.rst index ae51b8fd49..5e7e3d8b29 100755 --- a/docs/index.rst +++ b/docs/index.rst @@ -179,6 +179,7 @@ Indices and tables google_sheets google_cloud hustle + mailchimp mobilize_america newmode ngpvan diff --git a/docs/mailchimp.rst b/docs/mailchimp.rst new file mode 100644 index 0000000000..4a7823fdd4 --- /dev/null +++ b/docs/mailchimp.rst @@ -0,0 +1,32 @@ +Mailchimp +========= + +`Mailchimp `_ is a platform used for creating and sending mass emails. +`The Mailchimp API `_ allows users to interact with data from existing +email campaigns under their account and to configure further campaigns; this Parsons integration focuses on accessing +information about previous email campaigns and the recipients of those campaigns. + +*********** +Quick Start +*********** + +.. code-block:: python + + from parsons import Mailchimp + + mc = Mailchimp() + + # Get all recipient lists under a Mailchimp account + lists = mc.get_lists() + + # Get campaigns sent since the beginning of 2020 + recent_campaigns = mc.get_campaigns(since_send_time='2020-01-01T00:00:00Z') + + # Get all unsubscribes from a campaign + unsubscribes = mc.get_unsubscribes('dd693a3e74') + +*** +API +*** +.. autoclass :: parsons.mailchimp.Mailchimp + :inherited-members: \ No newline at end of file diff --git a/parsons/__init__.py b/parsons/__init__.py index cd2e727b55..faf307b223 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -41,6 +41,7 @@ from parsons.newmode.newmode import Newmode from parsons.databases.mysql.mysql import MySQL from parsons.rockthevote.rtv import RockTheVote + from parsons.mailchimp.mailchimp import Mailchimp from parsons.zoom.zoom import Zoom from parsons.action_network.action_network import ActionNetwork @@ -78,9 +79,10 @@ 'Newmode', 'MySQL', 'RockTheVote', + 'Mailchimp', 'Zoom', - 'ActionNetwork' - ] + 'ActionNetwork', + ] # Define the default logging config for Parsons and its submodules. For now the # logger gets a StreamHandler by default. At some point a NullHandler may be more diff --git a/parsons/mailchimp/__init__.py b/parsons/mailchimp/__init__.py new file mode 100644 index 0000000000..03bd942385 --- /dev/null +++ b/parsons/mailchimp/__init__.py @@ -0,0 +1,5 @@ +from parsons.mailchimp.mailchimp import Mailchimp + +__all__ = [ + 'Mailchimp' +] diff --git a/parsons/mailchimp/mailchimp.py b/parsons/mailchimp/mailchimp.py new file mode 100644 index 0000000000..e994944084 --- /dev/null +++ b/parsons/mailchimp/mailchimp.py @@ -0,0 +1,391 @@ +import logging +import re +from parsons.etl import Table +from parsons.utilities import check_env +from parsons.utilities.api_connector import APIConnector + +logger = logging.getLogger(__name__) + + +class Mailchimp(): + """ + Instantiate Mailchimp Class + + `Args:` + api_key: + The Mailchimp-provided application key. Not required if + ``MAILCHIMP_API_KEY`` env variable set. + `Returns:` + Mailchimp Class + """ + + def __init__(self, api_key=None): + self.api_key = check_env.check('MAILCHIMP_API_KEY', api_key) + self.domain = re.findall("(?<=-).+$", self.api_key)[0] + self.uri = f'https://{self.domain}.api.mailchimp.com/3.0/' + self.client = APIConnector(self.uri, auth=('x', self.api_key)) + + def get_request(self, endpoint, params=None, **kwargs): + # Internal method to make a get request. + + r = self.client.request(self.uri + endpoint, 'GET', params=params) + self.client.validate_response(r) + data = r.json() + + return data + + def get_lists(self, fields=None, exclude_fields=None, + count=None, offset=None, before_date_created=None, + since_date_created=None, before_campaign_last_sent=None, + since_campaign_last_sent=None, email=None, sort_field=None, + sort_dir=None): + """ + Get a table of lists under the account based on query parameters. Note + that argument descriptions here are sourced from Mailchimp's official + API documentation. + + `Args:` + fields: list of strings + A comma-separated list of fields to return. Reference + parameters of sub-objects with dot notation. + exclude_fields: list of strings + A comma-separated list of fields to exclude. Reference + parameters of sub-objects with dot notation. + count: int + The number of records to return. Default value is 10. Maximum + value is 1000. + offset: int + The number of records from a collection to skip. Iterating over + large collections with this parameter can be slow. Default + value is 0. + before_date_created: string + Restrict response to lists created before the set date. We + recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + since_date_created: string + Restrict results to lists created after the set date. We + recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + before_campaign_last_sent: string + Restrict results to lists created before the last campaign send + date. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + since_campaign_last_sent: string + Restrict results to lists created after the last campaign send + date. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + email: string + Restrict results to lists that include a specific subscriber's + email address. + sort_field: string, can only be 'date_created' or None + Returns files sorted by the specified field. + sort_dir: string, can only be 'ASC', 'DESC', or None + Determines the order direction for sorted results. + + `Returns:` + Table Class + """ + params = {'fields': fields, + 'exclude_fields': exclude_fields, + 'count': count, + 'offset': offset, + 'before_date_created': before_date_created, + 'since_date_created': since_date_created, + 'before_campaign_last_sent': before_campaign_last_sent, + 'since_campaign_last_sent': since_campaign_last_sent, + 'email': email, + 'sort_field': sort_field, + 'sort_dir': sort_dir} + + response = self.get_request('lists', params=params) + tbl = Table(response['lists']) + logger.info(f'Found {tbl.num_rows} lists.') + if tbl.num_rows > 0: + return tbl + else: + return Table() + + def get_campaigns(self, fields=None, exclude_fields=None, + count=None, offset=None, type=None, status=None, + before_send_time=None, since_send_time=None, + before_create_time=None, since_create_time=None, + list_id=None, folder_id=None, member_id=None, + sort_field=None, sort_dir=None): + """ + Get a table of campaigns under the account based on query parameters. + Note that argument descriptions here are sourced from Mailchimp's + official API documentation. + + `Args:` + fields: list of strings + A comma-separated list of fields to return. Reference + parameters of sub-objects with dot notation. + exclude_fields: list of strings + A comma-separated list of fields to exclude. Reference + parameters of sub-objects with dot notation. + count: int + The number of records to return. Default value is 10. Maximum + value is 1000. + offset: int + The number of records from a collection to skip. Iterating over + large collections with this parameter can be slow. Default + value is 0. + type: string, can only be 'regular', 'plaintext', 'absplit', 'rss', + 'variate', or None + The campaign type. + status: string, can only be 'save', 'paused', 'schedule', + 'sending', 'sent', or None + The status of the campaign. + before_send_time: string + Restrict the response to campaigns sent before the set time. We + recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + since_send_time: string + Restrict the response to campaigns sent after the set time. We + recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + before_create_time: string + Restrict the response to campaigns created before the set time. + We recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + since_create_time: string + Restrict the response to campaigns created after the set time. + We recommend ISO 8601 time format: 2015-10-21T15:41:36+00:00. + list_id: string + The unique id for the list. + folder_id: string + The unique folder id. + member_id: string + Retrieve campaigns sent to a particular list member. Member ID + is The MD5 hash of the lowercase version of the list member’s + email address. + sort_field: string, can only be 'create_time', 'send_time', or None + Returns files sorted by the specified field. + sort_dir: string, can only be 'ASC', 'DESC', or None + Determines the order direction for sorted results. + + `Returns:` + Table Class + """ + params = {'fields': fields, + 'exclude_fields': exclude_fields, + 'count': count, + 'offset': offset, + 'type': type, + 'status': status, + 'before_send_time': before_send_time, + 'since_send_time': since_send_time, + 'before_create_time': before_create_time, + 'since_create_time': since_create_time, + 'list_id': list_id, + 'folder_id': folder_id, + 'member_id': member_id, + 'sort_field': sort_field, + 'sort_dir': sort_dir} + + response = self.get_request('campaigns', params=params) + tbl = Table(response['campaigns']) + logger.info(f'Found {tbl.num_rows} campaigns.') + if tbl.num_rows > 0: + return tbl + else: + return Table() + + def get_members(self, list_id, fields=None, + exclude_fields=None, count=None, offset=None, + email_type=None, status=None, since_timestamp_opt=None, + before_timestamp_opt=None, since_last_changed=None, + before_last_changed=None, unique_email_id=None, + vip_only=False, interest_category_id=None, + interest_ids=None, interest_match=None, sort_field=None, + sort_dir=None, since_last_campaign=None, + unsubscribed_since=None): + """ + Get a table of members in a list based on query parameters. Note that + argument descriptions here are sourced from Mailchimp's official API + documentation. + + `Args:` + list_id: string + The unique ID of the list to fetch members from. + fields: list of strings + A comma-separated list of fields to return. Reference + parameters of sub-objects with dot notation. + exclude_fields: list of fields as strings + A comma-separated list of fields to exclude. Reference + parameters of sub-objects with dot notation. + count: int + The number of records to return. Default value is 10. Maximum + value is 1000. + offset: int + The number of records from a collection to skip. Iterating over + large collections with this parameter can be slow. Default + value is 0. + email_type: string + The email type. + status: string, can only be 'subscribed', 'unsubscribed', + 'cleaned', 'pending', 'transactional', 'archived', or None + The subscriber's status. + since_timestamp_opt: string + Restrict results to subscribers who opted-in after the set + timeframe. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + before_timestamp_opt: string + Restrict results to subscribers who opted-in before the set + timeframe. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + since_last_changed: string + Restrict results to subscribers whose information changed after + the set timeframe. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + before_last_changed: string + Restrict results to subscribers whose information changed + before the set timeframe. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + unique_email_id: string + A unique identifier for the email address across all Mailchimp + lists. This parameter can be found in any links with Ecommerce + Tracking enabled. + vip_only: boolean + A filter to return only the list's VIP members. Passing true + will restrict results to VIP list members, passing false will + return all list members. + interest_category_id: string + The unique id for the interest category. + interest_ids: list of strings + Used to filter list members by interests. Must be accompanied + by interest_category_id and interest_match. The value must be a + comma separated list of interest ids present for any supplied + interest categories. + interest_match: string, can only be 'any', 'all', 'none', or None + Used to filter list members by interests. Must be accompanied + by interest_category_id and interest_ids. "any" will match a + member with any of the interest supplied, "all" will only match + members with every interest supplied, and "none" will match + members without any of the interest supplied. + sort_field: string, can only be 'timestamp_opt', + 'timestamp_signup', 'last_changed', or None + Returns files sorted by the specified field. + sort_dir: string, can only be 'ASC', 'DESC', or None + Determines the order direction for sorted results. + since_last_campaign: string + Filter subscribers by those + subscribed/unsubscribed/pending/cleaned since last email + campaign send. Member status is required to use this filter. + unsubscribed_since: string + Filter subscribers by those unsubscribed since a specific date. + Using any status other than unsubscribed with this filter will + result in an error. + + `Returns:` + Table Class + """ + params = {'fields': fields, + 'exclude_fields': exclude_fields, + 'count': count, + 'offset': offset, + 'email_type': email_type, + 'status': status, + 'since_timestamp_opt': since_timestamp_opt, + 'before_timestamp_opt': before_timestamp_opt, + 'since_last_changed': since_last_changed, + 'before_last_changed': before_last_changed, + 'unqiue_email_id': unique_email_id, + 'vip_only': vip_only, + 'interest_category_id': interest_category_id, + 'interest_ids': interest_ids, + 'interest_match': interest_match, + 'sort_field': sort_field, + 'sort_dir': sort_dir, + 'since_last_campaign': since_last_campaign, + 'unsubscribed_since': unsubscribed_since} + + response = self.get_request(f'lists/{list_id}/members', params=params) + tbl = Table(response['members']) + logger.info(f'Found {tbl.num_rows} members.') + if tbl.num_rows > 0: + return tbl + else: + return Table() + + def get_campaign_emails(self, campaign_id, fields=None, + exclude_fields=None, count=None, offset=None, + since=None): + """ + Get a table of individual emails from a campaign based on query + parameters. Note that argument descriptions here are sourced from + Mailchimp's official API documentation. + + `Args:` + campaign_id: string + The unique ID of the campaign to fetch emails from. + fields: list of strings + A comma-separated list of fields to return. Reference + parameters of sub-objects with dot notation. + exclude_fields: list of strings + A comma-separated list of fields to exclude. Reference + parameters of sub-objects with dot notation. + count: int + The number of records to return. Default value is 10. Maximum + value is 1000. + offset: int + The number of records from a collection to skip. Iterating over + large collections with this parameter can be slow. Default + value is 0. + since: string + Restrict results to email activity events that occur after a + specific time. We recommend ISO 8601 time format: + 2015-10-21T15:41:36+00:00. + + `Returns:` + Table Class + """ + params = {'fields': fields, + 'exclude_fields': exclude_fields, + 'count': count, + 'offset': offset, + 'since': since} + + response = self.get_request(f'reports/{campaign_id}/email-activity', + params=params) + tbl = Table(response['emails']) + if tbl.num_rows > 0: + return tbl + else: + return Table() + + def get_unsubscribes(self, campaign_id, fields=None, + exclude_fields=None, count=None, offset=None): + """ + Get a table of unsubscribes associated with a campaign based on query + parameters. Note that argument descriptions here are sourced from + Mailchimp's official API documentation. + + `Args:` + campaign_id: string + The unique ID of the campaign to fetch unsubscribes from. + fields: list of strings + A comma-separated list of fields to return. Reference + parameters of sub-objects with dot notation. + exclude_fields: list of strings + A comma-separated list of fields to exclude. Reference + parameters of sub-objects with dot notation. + count: int + The number of records to return. Default value is 10. Maximum + value is 1000. + offset: int + The number of records from a collection to skip. Iterating over + large collections with this parameter can be slow. Default + value is 0. + + `Returns:` + Table Class + """ + params = {'fields': fields, + 'exclude_fields': exclude_fields, + 'count': count, + 'offset': offset} + + response = self.get_request(f'reports/{campaign_id}/unsubscribed', + params=params) + tbl = Table(response['unsubscribes']) + logger.info(f'Found {tbl.num_rows} unsubscribes for {campaign_id}.') + if tbl.num_rows > 0: + return tbl + else: + return Table() diff --git a/test/test_mailchimp/__init__.py b/test/test_mailchimp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/test_mailchimp/expected_json.py b/test/test_mailchimp/expected_json.py new file mode 100644 index 0000000000..d8d14fb152 --- /dev/null +++ b/test/test_mailchimp/expected_json.py @@ -0,0 +1,381 @@ +test_campaigns = { + "campaigns": [ + { + "id": "abc", + "web_id": 123, + "type": "regular", + "create_time": "2019-10-18T02:35:05+00:00", + "archive_url": "http://example.com/sample-campaign-1", + "long_archive_url": "https://mailchi.mp/abc/sample-campaign-1", + "status": "sent", + "emails_sent": 145, + "send_time": "2019-10-18T14:15:00+00:00", + "content_type": "template", + "needs_block_refresh": False, + "resendable": True, + "recipients": { + "list_id": "zyx", + "list_is_active": True, + "list_name": "Support Our Candidate List 1", + "segment_text": "", + "recipient_count": 145 + }, + "settings": { + "subject_line": "Sample Campaign 1", + "preview_text": "This is a sample campaign.", + "title": "Sample Campaign Donation Ask", + "from_name": "Our Candidate", + "reply_to": "our_candidate@example.com", + "use_conversation": False, + "to_name": "*|FNAME|* *|LNAME|*", + "folder_id": "", + "authenticate": True, + "auto_footer": False, + "inline_css": False, + "auto_tweet": False, + "fb_comments": True, + "timewarp": False, + "template_id": 12345, + "drag_and_drop": True + }, + "tracking": { + "opens": True, + "html_clicks": True, + "text_clicks": False, + "goal_tracking": False, + "ecomm360": False, + "google_analytics": "", + "clicktale": "" + }, + "report_summary": { + "opens": 48, + "unique_opens": 34, + "open_rate": 0.23776223776223776, + "clicks": 1, + "subscriber_clicks": 1, + "click_rate": 0.006993006993006993, + "ecommerce": { + "total_orders": 0, + "total_spent": 0, + "total_revenue": 0 + } + }, + "delivery_status": { + "enabled": False + }, + "_links": [] + }, + { + "id": "def", + "web_id": 456, + "type": "regular", + "create_time": "2019-05-29T11:46:41+00:00", + "archive_url": "http://example.com/sample-campaign-2", + "long_archive_url": "https://mailchi.mp/abc/sample-campaign-2", + "status": "sent", + "emails_sent": 87, + "send_time": "2019-05-29T21:18:15+00:00", + "content_type": "template", + "needs_block_refresh": False, + "resendable": True, + "recipients": { + "list_id": "wvu", + "list_is_active": True, + "list_name": "Support Our Candidate List 2", + "segment_text": "", + "recipient_count": 87 + }, + "settings": { + "subject_line": "Sample Campaign 2", + "preview_text": "This is another sample campaign.", + "title": "Sample Campaign 2 Donation Ask", + "from_name": "Our Candidate", + "reply_to": "our_candidate@example.com", + "use_conversation": False, + "to_name": "*|FNAME|* *|LNAME|*", + "folder_id": "", + "authenticate": True, + "auto_footer": False, + "inline_css": False, + "auto_tweet": False, + "fb_comments": True, + "timewarp": False, + "template_id": 67890, + "drag_and_drop": True + }, + "tracking": { + "opens": True, + "html_clicks": True, + "text_clicks": False, + "goal_tracking": False, + "ecomm360": False, + "google_analytics": "", + "clicktale": "" + }, + "report_summary": { + "opens": 108, + "unique_opens": 48, + "open_rate": 0.5647058823529412, + "clicks": 25, + "subscriber_clicks": 14, + "click_rate": 0.16470588235294117, + "ecommerce": { + "total_orders": 0, + "total_spent": 0, + "total_revenue": 0 + } + }, + "delivery_status": { + "enabled": False + }, + "_links": [] + }]} + +test_lists = { + "lists": [ + { + "id": "zyx", + "web_id": 98765, + "name": "Support Our Candidate List 1", + "contact": { + "company": "Support Our Candidate", + "address1": "123 Main Street", + "address2": "", + "city": "Townsville", + "state": "OH", + "zip": "43358", + "country": "US", + "phone": "" + }, + "permission_reminder": "You are receiving this email because you signed up at an event, while being canvassed, or on our website.", + "use_archive_bar": True, + "campaign_defaults": { + "from_name": "Our Candidate", + "from_email": "our_candidate@example.com", + "subject": "", + "language": "en" + }, + "notify_on_subscribe": "", + "notify_on_unsubscribe": "", + "date_created": "2019-03-25T22:55:44+00:00", + "list_rating": 3, + "email_type_option": False, + "subscribe_url_short": "http://example.com/sample-subscribe_url_2", + "subscribe_url_long": "https://mailchi.mp/zyx/sample-subscribe-url-2", + "beamer_address": "us00-sample-2@inbound.mailchimp.com", + "visibility": "pub", + "double_optin": False, + "has_welcome": True, + "marketing_permissions": False, + "modules": [], + "stats": { + "member_count": 140, + "unsubscribe_count": 8, + "cleaned_count": 16, + "member_count_since_send": 0, + "unsubscribe_count_since_send": 1, + "cleaned_count_since_send": 0, + "campaign_count": 21, + "campaign_last_sent": "2020-01-06T01:54:32+00:00", + "merge_field_count": 5, + "avg_sub_rate": 0, + "avg_unsub_rate": 1, + "target_sub_rate": 3, + "open_rate": 38.40236686390532, + "click_rate": 4.016786570743405, + "last_sub_date": "2019-09-24T01:07:56+00:00", + "last_unsub_date": "2020-01-06T01:55:02+00:00" + }, + "_links": [] + }, + { + "id": "xvu", + "web_id": 43210, + "name": "Support Our Candidate List 2", + "contact": { + "company": "Support Our Candidate", + "address1": "123 Main Street", + "address2": "", + "city": "Townsville", + "state": "OH", + "zip": "43358", + "country": "US", + "phone": "" + }, + "permission_reminder": "You are receiving this email because you signed up at an event, while being canvassed, or on our website.", + "use_archive_bar": True, + "campaign_defaults": { + "from_name": "Our Candidate", + "from_email": "our_candidate@example.com", + "subject": "", + "language": "en" + }, + "notify_on_subscribe": "", + "notify_on_unsubscribe": "", + "date_created": "2018-09-15T22:15:21+00:00", + "list_rating": 3, + "email_type_option": False, + "subscribe_url_short": "http://example.com/sample-subscribe_url_1", + "subscribe_url_long": "https://mailchi.mp/zyx/sample-subscribe-url-1", + "beamer_address": "us00-sample-1@inbound.mailchimp.com", + "visibility": "pub", + "double_optin": False, + "has_welcome": True, + "marketing_permissions": False, + "modules": [], + "stats": { + "member_count": 73, + "unsubscribe_count": 3, + "cleaned_count": 7, + "member_count_since_send": 1, + "unsubscribe_count_since_send": 1, + "cleaned_count_since_send": 0, + "campaign_count": 13, + "campaign_last_sent": "2020-01-03T14:38:11+00:00", + "merge_field_count": 5, + "avg_sub_rate": 0, + "avg_unsub_rate": 1, + "target_sub_rate": 3, + "open_rate": 64.19236186394533, + "click_rate": 3.746759370417411, + "last_sub_date": "2020-01-01T00:19:46+00:00", + "last_unsub_date": "2019-12-23T11:44:31+00:00" + }, + "_links": [] + }, + ], + "total_items": 1, + "constraints": { + "may_create": False, + "max_instances": 1, + "current_total_instances": 1 + }, + "_links": [] +} + +test_members = { + "members": [ + { + "id": "9eb69db8d0371811aa18803a1ae21584", + "email_address": "member_1@example.com", + "unique_email_id": "c82a25d939", + "web_id": 24816326, + "email_type": "html", + "status": "subscribed", + "merge_fields": { + "FNAME": "Member", + "LNAME": "One", + "ADDRESS": { + "addr1": "", + "addr2": "", + "city": "", + "state": "", + "zip": "", + "country": "US" + }, + "PHONE": "", + "BIRTHDAY": "" + }, + "stats": { + "avg_open_rate": 0.3571, + "avg_click_rate": 0 + }, + "ip_signup": "", + "timestamp_signup": "", + "ip_opt": "174.59.50.35", + "timestamp_opt": "2019-03-25T22:55:44+00:00", + "member_rating": 4, + "last_changed": "2019-03-25T22:55:44+00:00", + "language": "en", + "vip": False, + "email_client": "Gmail", + "location": { + "latitude": 40.0293, + "longitude": -76.2656, + "gmtoff": 0, + "dstoff": 0, + "country_code": "US", + "timezone": "717/223" + }, + "source": "Unknown", + "tags_count": 0, + "tags": [], + "list_id": "67fdf4b1f4", + "_links": [] + }, + { + "id": "4f315641dbad7b74acc0f4a5d3741ac6", + "email_address": "member_2@example.com", + "unique_email_id": "8d308d69d3", + "web_id": 12233445, + "email_type": "html", + "status": "subscribed", + "merge_fields": { + "FNAME": "Member", + "LNAME": "Two", + "ADDRESS": "", + "PHONE": "", + "BIRTHDAY": "" + }, + "stats": { + "avg_open_rate": 0.5, + "avg_click_rate": 0 + }, + "ip_signup": "", + "timestamp_signup": "", + "ip_opt": "174.59.50.35", + "timestamp_opt": "2019-03-25T23:04:46+00:00", + "member_rating": 4, + "last_changed": "2019-03-25T23:04:46+00:00", + "language": "", + "vip": False, + "email_client": "iPhone", + "location": { + "latitude": 40.0459, + "longitude": -76.3542, + "gmtoff": 0, + "dstoff": 0, + "country_code": "US", + "timezone": "717/223" + }, + "source": "Import", + "tags_count": 2, + "tags": [ + { + "id": 17493, + "name": "canvass" + }, + { + "id": 17497, + "name": "canvass-03-17-2019" + } + ], + "list_id": "67fdf4b1f4", + "_links": [] + }]} + +test_unsubscribes = { + "unsubscribes": [ + { + "email_id": "e542e5cd7b414e5ff8409ff57cf154be", + "email_address": "unsubscribe_1@exmaple.com", + "merge_fields": { + "FNAME": "Unsubscriber", + "LNAME": "One", + "ADDRESS": "", + "PHONE": "5558754307", + "BIRTHDAY": "" + }, + "vip": False, + "timestamp": "2019-12-09T21:18:06+00:00", + "reason": "None given", + "campaign_id": "abc", + "list_id": "zyx", + "list_is_active": True, + "_links": [] + } + ], + "campaign_id": "abc", + "total_items": 1, + "_links": [] +} \ No newline at end of file diff --git a/test/test_mailchimp/test_mailchimp.py b/test/test_mailchimp/test_mailchimp.py new file mode 100644 index 0000000000..6b32e89d64 --- /dev/null +++ b/test/test_mailchimp/test_mailchimp.py @@ -0,0 +1,49 @@ +from parsons.mailchimp.mailchimp import Mailchimp +import unittest +import requests_mock +from test.utils import assert_matching_tables +from test.test_mailchimp import expected_json + +API_KEY = 'mykey-us00' + +class TestMailchimp(unittest.TestCase): + + def setUp(self): + + self.mc = Mailchimp(API_KEY) + + @requests_mock.Mocker() + def test_get_campaigns(self, m): + + # Test that campaigns are returned correctly. + m.get(self.mc.uri + 'campaigns', json=expected_json.test_campaigns) + tbl = self.mc.get_campaigns() + + self.assertEqual(tbl.num_rows, 2) + + @requests_mock.Mocker() + def test_get_lists(self, m): + + # Test that lists are returned correctly. + m.get(self.mc.uri + 'lists', json=expected_json.test_lists) + tbl = self.mc.get_lists() + + self.assertEqual(tbl.num_rows, 2) + + @requests_mock.Mocker() + def test_get_members(self, m): + + # Test that list members are returned correctly. + m.get(self.mc.uri + 'lists/zyx/members', json=expected_json.test_members) + tbl = self.mc.get_members(list_id='zyx') + + self.assertEqual(tbl.num_rows, 2) + + @requests_mock.Mocker() + def test_get_unsubscribes(self, m): + + # Test that campaign unsubscribes are returned correctly. + m.get(self.mc.uri + 'reports/abc/unsubscribed', json=expected_json.test_unsubscribes) + tbl = self.mc.get_unsubscribes(campaign_id='abc') + + self.assertEqual(tbl.num_rows, 1)