├── lib ├── __init__.py └── createsend │ ├── journey.py │ ├── __init__.py │ ├── template.py │ ├── administrator.py │ ├── person.py │ ├── journey_email.py │ ├── segment.py │ ├── transactional.py │ ├── subscriber.py │ ├── utils.py │ ├── client.py │ ├── campaign.py │ └── list.py ├── test ├── __init__.py ├── fixtures │ ├── add_subscriber.json │ ├── create_custom_field.json │ ├── billingdetails.json │ ├── create_template.json │ ├── update_custom_field.json │ ├── create_campaign.json │ ├── create_client.json │ ├── create_list.json │ ├── create_list_webhook.json │ ├── create_segment.json │ ├── apikey.json │ ├── systemdate.json │ ├── add_admin.json │ ├── add_person.json │ ├── admin_get_primary_contact.json │ ├── admin_set_primary_contact.json │ ├── client_get_primary_contact.json │ ├── client_set_primary_contact.json │ ├── custom_api_error.json │ ├── sample_client_error.json │ ├── transfer_credits.json │ ├── expired_oauth_token_api_error.json │ ├── sample_server_error.json │ ├── external_session.json │ ├── admin_details.json │ ├── oauth_exchange_token_error.json │ ├── oauth_exchange_token.json │ ├── person_details.json │ ├── refresh_oauth_token.json │ ├── tags.json │ ├── import_subscribers.json │ ├── tx_send_single.json │ ├── lists.json │ ├── clients.json │ ├── list_details.json │ ├── administrators.json │ ├── people.json │ ├── template_details.json │ ├── segments.json │ ├── tx_statistics_smart.json │ ├── tx_statistics_classic.json │ ├── campaign_listsandsegments.json │ ├── tx_classicemail_groups.json │ ├── tx_send_multiple.json │ ├── custom_fields_utf8.json │ ├── client_journeys.json │ ├── campaign_spam.json │ ├── listsforemail.json │ ├── tx_smartemails.json │ ├── journey_email_unsubscribes_no_params.json │ ├── journey_email_unsubscribes_with_params.json │ ├── list_webhooks.json │ ├── tx_messages_classic.json │ ├── campaign_unsubscribes.json │ ├── journey_summary.json │ ├── tx_messages_smart.json │ ├── import_subscribers_partial_success.json │ ├── campaign_summary.json │ ├── subscriber_details.json │ ├── bounced_subscribers.json │ ├── custom_fields.json │ ├── subscriber_details_with_tracking_preference.json │ ├── segment_details.json │ ├── journey_email_bounces_no_params.json │ ├── journey_email_bounces_with_params.json │ ├── templates.json │ ├── segment_subscribers.json │ ├── campaign_bounces.json │ ├── journey_email_recipients_no_params.json │ ├── journey_email_recipients_with_params.json │ ├── list_stats.json │ ├── client_details.json │ ├── segment_subscribers_with_tracking_preference.json │ ├── tx_smartemail_details.json │ ├── drafts.json │ ├── email_client_usage.json │ ├── journey_email_opens_no_params.json │ ├── journey_email_opens_with_params.json │ ├── journey_email_clicks_no_params.json │ ├── journey_email_clicks_with_params.json │ ├── tx_message_details.json │ ├── scheduled_campaigns.json │ ├── suppressionlist.json │ ├── subscriber_history.json │ ├── tx_messages.json │ ├── unconfirmed_subscribers.json │ ├── campaigns.json │ ├── campaign_clicks.json │ ├── deleted_subscribers.json │ ├── unsubscribed_subscribers.json │ ├── campaign_opens.json │ ├── tx_message_details_with_statistics.json │ ├── active_subscribers.json │ ├── active_subscribers_with_tracking_preference.json │ ├── campaign_recipients.json │ ├── timezones.json │ └── countries.json ├── test_verifiedhttpsconnection.py ├── test_journey.py ├── test_template.py ├── test_administrator.py ├── test_people.py ├── test_segment.py ├── test_authentication.py ├── test_transactional.py ├── test_journey_email.py └── test_subscriber.py ├── setup.cfg ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── samples ├── general.py ├── campaigns.py ├── segments.py ├── lists.py ├── subscribers.py └── clients.py ├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── Rakefile ├── tox.ini ├── CONTRIBUTING.md ├── LICENSE ├── setup.py ├── RELEASE.md └── README.md /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /test/fixtures/add_subscriber.json: -------------------------------------------------------------------------------- 1 | "subscriber@example.com" -------------------------------------------------------------------------------- /test/fixtures/create_custom_field.json: -------------------------------------------------------------------------------- 1 | "[newdatefield]" -------------------------------------------------------------------------------- /test/fixtures/billingdetails.json: -------------------------------------------------------------------------------- 1 | { 2 | "Credits": 3021 3 | } -------------------------------------------------------------------------------- /test/fixtures/create_template.json: -------------------------------------------------------------------------------- 1 | "98y2e98y289dh89h9383891234" -------------------------------------------------------------------------------- /test/fixtures/update_custom_field.json: -------------------------------------------------------------------------------- 1 | "[myrenamedcustomfield]" -------------------------------------------------------------------------------- /test/fixtures/create_campaign.json: -------------------------------------------------------------------------------- 1 | "787y87y87y87y87y87y8712341234" -------------------------------------------------------------------------------- /test/fixtures/create_client.json: -------------------------------------------------------------------------------- 1 | "32a381c49a2df99f1d0c6f3c112352b9" -------------------------------------------------------------------------------- /test/fixtures/create_list.json: -------------------------------------------------------------------------------- 1 | "e3c5f034d68744f7881fdccf13c2daee1234" -------------------------------------------------------------------------------- /test/fixtures/create_list_webhook.json: -------------------------------------------------------------------------------- 1 | "6a783d359bd44ef62c6ca0d3eda4412a" -------------------------------------------------------------------------------- /test/fixtures/create_segment.json: -------------------------------------------------------------------------------- 1 | "0246c2aea610a3545d9780bf6ab890061234" -------------------------------------------------------------------------------- /test/fixtures/apikey.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiKey": "981298u298ue98u219e8u2e98u2" 3 | } -------------------------------------------------------------------------------- /test/fixtures/systemdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "SystemDate": "2010-10-15 09:27:00" 3 | } -------------------------------------------------------------------------------- /test/fixtures/add_admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "admin@example.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/add_person.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "person@example.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/admin_get_primary_contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "admin@blackhole.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/admin_set_primary_contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "admin@blackhole.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/client_get_primary_contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "person@blackhole.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/client_set_primary_contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "person@blackhole.com" 3 | } -------------------------------------------------------------------------------- /test/fixtures/custom_api_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "Code": 98798, 3 | "Message": "A crazy API error" 4 | } -------------------------------------------------------------------------------- /test/fixtures/sample_client_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "Code": 418, 3 | "Message": " I'm a teapot" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/transfer_credits.json: -------------------------------------------------------------------------------- 1 | { 2 | "AccountCredits": 800, 3 | "ClientCredits": 200 4 | } -------------------------------------------------------------------------------- /test/fixtures/expired_oauth_token_api_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "Code": 121, 3 | "Message": "Expired OAuth Token" 4 | } -------------------------------------------------------------------------------- /test/fixtures/sample_server_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "Code": 500, 3 | "Message": "Internal Server Error" 4 | } 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.py 3 | include *.txt 4 | include LICENSE 5 | recursive-include lib *.py 6 | -------------------------------------------------------------------------------- /test/fixtures/external_session.json: -------------------------------------------------------------------------------- 1 | { 2 | "SessionUrl": "https://external1.createsend.com/cd/create/ABCDEF12/DEADBEEF?url=FEEDDAD1" 3 | } -------------------------------------------------------------------------------- /test/fixtures/admin_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "admin@example.com", 3 | "Name": "Admin One", 4 | "Status": "Active" 5 | } -------------------------------------------------------------------------------- /test/fixtures/oauth_exchange_token_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "invalid_grant", 3 | "error_description": "Specified code was invalid or expired" 4 | } -------------------------------------------------------------------------------- /test/fixtures/oauth_exchange_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "SlAV32hkKG", 3 | "expires_in": 1209600, 4 | "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA" 5 | } -------------------------------------------------------------------------------- /test/fixtures/person_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "person@example.com", 3 | "Name": "Person One", 4 | "AccessLevel": 1023, 5 | "Status": "Active" 6 | } -------------------------------------------------------------------------------- /test/fixtures/refresh_oauth_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "SlAV32hkKG2e12e", 3 | "expires_in": 1209600, 4 | "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA" 5 | } -------------------------------------------------------------------------------- /test/fixtures/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Happy", 4 | "NumberOfCampaigns": 3 5 | }, 6 | { 7 | "Name": "Sad", 8 | "NumberOfCampaigns": 1 9 | } 10 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | example.py 4 | build 5 | dist 6 | venv 7 | .coverage 8 | cover 9 | htmlcov 10 | .tox 11 | lib/createsend.egg-info 12 | .rake_tasks~ 13 | .idea -------------------------------------------------------------------------------- /test/fixtures/import_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "FailureDetails": [], 3 | "TotalUniqueEmailsSubmitted": 3, 4 | "TotalExistingSubscribers": 0, 5 | "TotalNewSubscribers": 3, 6 | "DuplicateEmailsInSubmission": [] 7 | } -------------------------------------------------------------------------------- /test/fixtures/tx_send_single.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "MessageID": "0cfe150d-d507-11e4-84a7-c31e5b59881d", 4 | "Recipient": "\"Bob Sacamano\" ", 5 | "Status": "Received" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /test/fixtures/lists.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ListID": "a58ee1d3039b8bec838e6d1482a8a965", 4 | "Name": "List One" 5 | }, 6 | { 7 | "ListID": "99bc35084a5739127a8ab81eae5bd305", 8 | "Name": "List Two" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /test/fixtures/clients.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ClientID": "4a397ccaaa55eb4e6aa1221e1e2d7122", 4 | "Name": "Client One" 5 | }, 6 | { 7 | "ClientID": "a206def0582eec7dae47d937a4109cb2", 8 | "Name": "Client Two" 9 | } 10 | ] -------------------------------------------------------------------------------- /test/fixtures/list_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConfirmedOptIn": false, 3 | "Title": "a non-basic list :)", 4 | "UnsubscribePage": "", 5 | "ListID": "2fe4c8f0373ce320e2200596d7ef168f", 6 | "ConfirmationSuccessPage": "", 7 | "UnsubscribeSetting": "AllClientLists" 8 | } -------------------------------------------------------------------------------- /test/fixtures/administrators.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "EmailAddress": "admin1@blackhole.com", 4 | "Name": "Admin One", 5 | "Status": "Active" 6 | }, 7 | { 8 | "EmailAddress": "admin2@blackhole.com", 9 | "Name": "Admin Two", 10 | "Status": "Active" 11 | } 12 | ] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | - 3.7 5 | - 3.8 6 | - 3.9 7 | - 3.10 8 | - 3.11 9 | - 3.12 10 | - 3.13 11 | install: 12 | - pip install coverage coveralls 13 | script: 14 | - coverage run --source=lib setup.py test 15 | - coverage report 16 | - coveralls 17 | sudo: false 18 | -------------------------------------------------------------------------------- /samples/general.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | 7 | cs = CreateSend(auth) 8 | clients = cs.clients() 9 | 10 | # Get list of clients 11 | for cl in clients: 12 | print(f"Client: {cl.Name} - Id: {cl.ClientID}") -------------------------------------------------------------------------------- /test/fixtures/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "EmailAddress": "person1@blackhole.com", 4 | "Name": "Person One", 5 | "Status": "Active", 6 | "AccessLevel": 31 7 | }, 8 | { 9 | "EmailAddress": "person2@blackhole.com", 10 | "Name": "Person Two", 11 | "Status": "Active", 12 | "AccessLevel": 0 13 | } 14 | ] -------------------------------------------------------------------------------- /test/fixtures/template_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "TemplateID": "98y2e98y289dh89h938389", 3 | "Name": "Template One", 4 | "PreviewURL": "https://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=01AF532CD8889B33&d=r&c=E816F55BFAD1A753", 5 | "ScreenshotURL": "https://preview.createsend.com/ts/r/14/833/263/14833263.jpg?0318092600" 6 | } -------------------------------------------------------------------------------- /test/fixtures/segments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ListID": "a58ee1d3039b8bec838e6d1482a8a965", 4 | "SegmentID": "46aa5e01fd43381863d4e42cf277d3a9", 5 | "Title": "Segment One" 6 | }, 7 | { 8 | "ListID": "8dffb94c60c5faa3d40f496f2aa58a8a", 9 | "SegmentID": "dhw9q8jd9q8wd09quw0d909wid9i09iq", 10 | "Title": "Segment Two" 11 | } 12 | ] -------------------------------------------------------------------------------- /test/fixtures/tx_statistics_smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Query": { 3 | "SmartEmailID": "bb4a6ebb-663d-42a0-bdbe-60512cf30a01", 4 | "From": "2014-02-03", 5 | "To": "2015-02-02", 6 | "TimeZone": "UTC" 7 | }, 8 | "Sent": 1000, 9 | "Bounces": 8, 10 | "Delivered": 992, 11 | "Opened": 300, 12 | "Clicked": 50 13 | } 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/tx_statistics_classic.json: -------------------------------------------------------------------------------- 1 | { 2 | "Query": { 3 | "Group": "Password Reset", 4 | "From": "2014-02-03", 5 | "To": "2015-02-02", 6 | "TimeZone": "(GMT+10:00) Canberra, Melbourne, Sydney" 7 | }, 8 | "Sent": 1000, 9 | "Bounces": 8, 10 | "Delivered": 992, 11 | "Opened": 300, 12 | "Clicked": 50 13 | } 14 | 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Configure Dependabot scanning. 2 | version: 2 3 | 4 | updates: 5 | # Check for updates to GitHub Actions. 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | open-pull-requests-limit: 10 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /test/fixtures/campaign_listsandsegments.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lists": [ 3 | { 4 | "ListID": "a58ee1d3039b8bec838e6d1482a8a965", 5 | "Name": "List One" 6 | } 7 | ], 8 | "Segments": [ 9 | { 10 | "ListID": "2bea949d0bf96148c3e6a209d2e82060", 11 | "SegmentID": "dba84a225d5ce3d19105d7257baac46f", 12 | "Title": "Segment for campaign" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /test/fixtures/tx_classicemail_groups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Group": "Password Reset", 4 | "CreatedAt": "2015-03-28T09:41:36+11:00" 5 | }, 6 | { 7 | "Group": "Credit card expired", 8 | "CreatedAt": "2015-05-04T22:24:12+10:00" 9 | }, 10 | { 11 | "Group": "Order shipped", 12 | "CreatedAt": "2015-03-29T11:11:52+11:00" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /samples/campaigns.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | campaignId = 'YOUR_CAMPAIGN_ID' 7 | 8 | campaign = Campaign(auth, campaignId) 9 | 10 | # Get the summary info for a campaign 11 | summary = campaign.summary() 12 | for property, value in vars(summary).items(): 13 | print(property, ":", value) -------------------------------------------------------------------------------- /test/fixtures/tx_send_multiple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "MessageID": "0cfe150d-d507-11e4-84a7-c31e5b59881d", 4 | "Recipient": "\"Bob Sacamano\" ", 5 | "Status": "Received" 6 | }, 7 | { 8 | "MessageID": "0cfe150d-d507-11e4-b579-a64eb0d9c74d", 9 | "Recipient": "\"Newman\" ", 10 | "Status": "Received" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /samples/segments.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | segmentId = 'YOUR_SEGMENT_ID' 7 | 8 | segment = Segment(auth, segmentId) 9 | 10 | # Get list of active subscribers in a segment 11 | print("List of active subscribers:") 12 | for cm in segment.subscribers().Results: 13 | print(" - %s" % cm.EmailAddress) -------------------------------------------------------------------------------- /test/fixtures/custom_fields_utf8.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "FieldName": "salary_range", 4 | "Key": "[]", 5 | "DataType": "MultiSelectOne", 6 | "FieldOptions": ["£0-20k", "£20-30k", "£30k+"], 7 | "VisibleInPreferenceCenter": true 8 | }, 9 | { 10 | "FieldName": "age", 11 | "Key": "[age]", 12 | "DataType": "Number", 13 | "FieldOptions": [], 14 | "VisibleInPreferenceCenter": true 15 | } 16 | ] -------------------------------------------------------------------------------- /test/fixtures/client_journeys.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ListID": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", 4 | "JourneyID": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", 5 | "Name": "Journey 1", 6 | "Status": "Active" 7 | }, 8 | { 9 | "ListID": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", 10 | "JourneyID": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", 11 | "Name": "New journey", 12 | "Status": "Active" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/fixtures/campaign_spam.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+6576576576@example.com", 5 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 6 | "Date": "2010-10-11 08:29:00" 7 | } 8 | ], 9 | "ResultsOrderedBy": "date", 10 | "OrderDirection": "asc", 11 | "PageNumber": 1, 12 | "PageSize": 1000, 13 | "RecordsOnThisPage": 1, 14 | "TotalNumberOfRecords": 1, 15 | "NumberOfPages": 1 16 | } -------------------------------------------------------------------------------- /test/fixtures/listsforemail.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ListID": "ab4a2b57c7c8f1ba62f898a1af1a575b", 4 | "ListName": "List Number One", 5 | "SubscriberState": "Active", 6 | "DateSubscriberAdded": "2012-08-20 22:32:00" 7 | }, 8 | { 9 | "ListID": "d8e59b07138cf1316a6587007c443e21", 10 | "ListName": "List Number Two", 11 | "SubscriberState": "Unsubscribed", 12 | "DateSubscriberAdded": "2012-08-21 01:35:00" 13 | } 14 | ] -------------------------------------------------------------------------------- /test/fixtures/tx_smartemails.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "1e654df2-f484-11e4-970c-6c4008bc7468", 4 | "Name": "Welcome email", 5 | "CreatedAt": "2015-05-15T16:09:19-05:00", 6 | "Status": "Active" 7 | }, 8 | { 9 | "ID": "21dab350-f484-11e4-ad38-6c4008bc7468", 10 | "Name": "Order shipped", 11 | "CreatedAt": "2015-05-15T13:29:49-05:00", 12 | "Status": "Draft" 13 | } 14 | ] 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_unsubscribes_no_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@example.com", 5 | "Date": "2019-08-19 10:24:00", 6 | "IPAddress": "198.148.196.144" 7 | } 8 | ], 9 | "ResultsOrderedBy": "Date", 10 | "OrderDirection": "ASC", 11 | "PageNumber": 1, 12 | "PageSize": 1000, 13 | "RecordsOnThisPage": 1, 14 | "TotalNumberOfRecords": 1, 15 | "NumberOfPages": 1 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_unsubscribes_with_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@example.com", 5 | "Date": "2019-08-19 10:24:00", 6 | "IPAddress": "198.148.196.144" 7 | } 8 | ], 9 | "ResultsOrderedBy": "Date", 10 | "OrderDirection": "DESC", 11 | "PageNumber": 1, 12 | "PageSize": 10, 13 | "RecordsOnThisPage": 1, 14 | "TotalNumberOfRecords": 1, 15 | "NumberOfPages": 1 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/list_webhooks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "WebhookID": "943678317049bc13", 4 | "Events": [ "Deactivate" ], 5 | "Url": "https://www.postbin.org/d9w8ud9wud9w", 6 | "Status": "Active", 7 | "PayloadFormat": "Json" 8 | }, 9 | { 10 | "WebhookID": "ee1b3864e5ca6161", 11 | "Events": [ 12 | "Subscribe" 13 | ], 14 | "Url": "https://www.postbin.org/hiuhiu2h2u", 15 | "Status": "Active", 16 | "PayloadFormat": "Xml" 17 | } 18 | ] -------------------------------------------------------------------------------- /test/fixtures/tx_messages_classic.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 4 | "Status": "Delivered", 5 | "SentAt": "2014-01-15T16:09:19-05:00", 6 | "Recipient": "Joe Smith ", 7 | "From": "Team ", 8 | "Subject": "Your password has been reset", 9 | "TotalOpens": 2, 10 | "TotalClicks": 4, 11 | "CanBeResent": true, 12 | "Group": "Password Reset" 13 | } 14 | ] 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/campaign_unsubscribes.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+6576576576@example.com", 5 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 6 | "Date": "2010-10-11 08:29:00", 7 | "IPAddress": "192.168.126.87" 8 | } 9 | ], 10 | "ResultsOrderedBy": "date", 11 | "OrderDirection": "asc", 12 | "PageNumber": 1, 13 | "PageSize": 1000, 14 | "RecordsOnThisPage": 1, 15 | "TotalNumberOfRecords": 1, 16 | "NumberOfPages": 1 17 | } -------------------------------------------------------------------------------- /test/fixtures/journey_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "JourneyID": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", 3 | "Name": "New journey", 4 | "TriggerType": "On Subscription", 5 | "Status": "Active", 6 | "Emails": [ 7 | { 8 | "EmailID": "b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1", 9 | "Name": "1", 10 | "Bounced": 7, 11 | "Clicked": 1, 12 | "Opened": 12, 13 | "Sent": 11, 14 | "UniqueOpened": 4, 15 | "Unsubscribed": 1 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/tx_messages_smart.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 4 | "Status": "Delivered", 5 | "SentAt": "2014-01-15T16:09:19-05:00", 6 | "Recipient": "Joe Smith ", 7 | "From": "Team ", 8 | "Subject": "Your credit card has expired", 9 | "TotalOpens": 2, 10 | "TotalClicks": 4, 11 | "CanBeResent": true, 12 | "SmartEmailID": "21dab350-f484-11e4-ad38-6c4008bc7468" 13 | } 14 | ] 15 | 16 | -------------------------------------------------------------------------------- /test/fixtures/import_subscribers_partial_success.json: -------------------------------------------------------------------------------- 1 | { 2 | "ResultData": { 3 | "TotalUniqueEmailsSubmitted": 3, 4 | "TotalExistingSubscribers": 2, 5 | "TotalNewSubscribers": 0, 6 | "DuplicateEmailsInSubmission": [], 7 | "FailureDetails": [ 8 | { 9 | "EmailAddress": "example+1@example", 10 | "Code": 1, 11 | "Message": "Invalid Email Address" 12 | } 13 | ] 14 | }, 15 | "Code": 210, 16 | "Message": "Subscriber Import had some failures" 17 | } -------------------------------------------------------------------------------- /test/fixtures/campaign_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Last Campaign", 3 | "Recipients": 5, 4 | "TotalOpened": 10, 5 | "Clicks": 0, 6 | "Unsubscribed": 0, 7 | "Bounced": 0, 8 | "UniqueOpened": 5, 9 | "Mentions": 23, 10 | "Forwards": 11, 11 | "Likes": 32, 12 | "WebVersionURL": "https://createsend.com/t/r-3A433FC72FFE3B8B", 13 | "WebVersionTextURL": "https://createsend.com/t/r-3A433FC72FFE3B8B/t", 14 | "WorldviewURL": "https://client.createsend.com/reports/wv/r/3A433FC72FFE3B8B", 15 | "SpamComplaints": 23 16 | } -------------------------------------------------------------------------------- /test/fixtures/subscriber_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "subscriber@example.com", 3 | "Name": "Subscriber One", 4 | "Date": "2010-10-25 10:28:00", 5 | "ListJoinedDate": "2010-10-25 10:28:00", 6 | "State": "Active", 7 | "CustomFields": [ 8 | { 9 | "Key": "website", 10 | "Value": "https://example.com" 11 | }, 12 | { 13 | "Key": "age", 14 | "Value": "24" 15 | }, 16 | { 17 | "Key": "subscription date", 18 | "Value": "2010-03-09" 19 | } 20 | ], 21 | "ReadsEmailWith": "Gmail" 22 | } -------------------------------------------------------------------------------- /lib/createsend/journey.py: -------------------------------------------------------------------------------- 1 | from createsend.createsend import CreateSendBase 2 | from createsend.utils import json_to_py 3 | 4 | 5 | class Journey(CreateSendBase): 6 | """Represents a journey and associated functionality""" 7 | 8 | def __init__(self, auth=None, journey_id=None): 9 | self.journey_id = journey_id 10 | super().__init__(auth) 11 | 12 | def summary(self): 13 | """Gets the summary of the journey""" 14 | response = self._get("/journeys/%s.json" % self.journey_id) 15 | return json_to_py(response) 16 | -------------------------------------------------------------------------------- /test/fixtures/bounced_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "bouncedsubscriber@example.com", 5 | "Name": "Bounced One", 6 | "Date": "2010-10-25 13:11:00", 7 | "ListJoinedDate": "2010-10-25 13:11:00", 8 | "State": "Bounced", 9 | "CustomFields": [], 10 | "ReadsEmailWith": "" 11 | } 12 | ], 13 | "ResultsOrderedBy": "email", 14 | "OrderDirection": "asc", 15 | "PageNumber": 1, 16 | "PageSize": 1000, 17 | "RecordsOnThisPage": 1, 18 | "TotalNumberOfRecords": 1, 19 | "NumberOfPages": 1 20 | } -------------------------------------------------------------------------------- /test/fixtures/custom_fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "FieldName": "website", 4 | "Key": "[website]", 5 | "DataType": "Text", 6 | "FieldOptions": [], 7 | "VisibleInPreferenceCenter": true 8 | }, 9 | { 10 | "FieldName": "age", 11 | "Key": "[age]", 12 | "DataType": "Number", 13 | "FieldOptions": [], 14 | "VisibleInPreferenceCenter": true 15 | }, 16 | { 17 | "FieldName": "subscription date", 18 | "Key": "[subscriptiondate]", 19 | "DataType": "Date", 20 | "FieldOptions": [], 21 | "VisibleInPreferenceCenter": false 22 | } 23 | ] -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | desc "Run tests" 3 | task :test do 4 | system "pip install tox" 5 | system "tox --skip-missing-interpreters" 6 | end 7 | 8 | desc "Build source and wheel distributions" 9 | task :build do 10 | system "python setup.py sdist" 11 | system "python setup.py bdist_wheel" 12 | end 13 | 14 | desc "Build and release a source distribution" 15 | task :release do 16 | # Create source and wheel distributions 17 | system "python setup.py sdist bdist_wheel" 18 | 19 | # Upload using Twine 20 | system "python -m twine upload dist/*" 21 | end 22 | 23 | task :default => :test 24 | -------------------------------------------------------------------------------- /test/fixtures/subscriber_details_with_tracking_preference.json: -------------------------------------------------------------------------------- 1 | { 2 | "EmailAddress": "subscriber@example.com", 3 | "Name": "Subscriber One", 4 | "Date": "2010-10-25 10:28:00", 5 | "ListJoinedDate": "2010-10-25 10:28:00", 6 | "State": "Active", 7 | "CustomFields": [ 8 | { 9 | "Key": "website", 10 | "Value": "https://example.com" 11 | }, 12 | { 13 | "Key": "age", 14 | "Value": "24" 15 | }, 16 | { 17 | "Key": "subscription date", 18 | "Value": "2010-03-09" 19 | } 20 | ], 21 | "ReadsEmailWith": "Gmail", 22 | "ConsentToTrack": "Yes" 23 | } -------------------------------------------------------------------------------- /test/fixtures/segment_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "ActiveSubscribers": 0, 3 | "RuleGroups": [ 4 | { 5 | "Rules": [ 6 | { 7 | "RuleType": "EmailAddress", 8 | "Clause": "CONTAINS @hello.com" 9 | } 10 | ] 11 | }, 12 | { 13 | "Rules": [ 14 | { 15 | "RuleType": "Name", 16 | "Clause": "PROVIDED" 17 | } 18 | ] 19 | } 20 | ], 21 | "ListID": "2bea949d0bf96148c3e6a209d2e82060", 22 | "SegmentID": "dba84a225d5ce3d19105d7257baac46f", 23 | "Title": "My Segment" 24 | } -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (https://tox.wiki/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | env_list = py38, py39, py310, py311, py312, py313 8 | 9 | [testenv] 10 | commands = 11 | pytest --cov=lib 12 | deps = 13 | pytest-cov 14 | # This needs to be set to include the test fixtures 15 | use_develop = true 16 | 17 | [gh] 18 | # Maps python versions for GitHub workflow 19 | python = 20 | 3.8: py38 21 | 3.9: py39 22 | 3.10: py310 23 | 3.11: py311 24 | 3.12: py312 25 | 3.13: py313 -------------------------------------------------------------------------------- /test/fixtures/journey_email_bounces_no_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@softbouncemyemail.comX", 5 | "BounceType": "Soft", 6 | "Date": "2019-08-20 14:24:00", 7 | "Reason": "Soft Bounce - Dns Failure" 8 | }, 9 | { 10 | "EmailAddress": "asdf@hardbouncemyemail.com", 11 | "BounceType": "Hard", 12 | "Date": "2019-08-21 04:26:00", 13 | "Reason": "Hard Bounce" 14 | } 15 | ], 16 | "ResultsOrderedBy": "Date", 17 | "OrderDirection": "ASC", 18 | "PageNumber": 1, 19 | "PageSize": 1000, 20 | "RecordsOnThisPage": 2, 21 | "TotalNumberOfRecords": 2, 22 | "NumberOfPages": 1 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_bounces_with_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@hardbouncemyemail.com", 5 | "BounceType": "Hard", 6 | "Date": "2019-08-21 04:26:00", 7 | "Reason": "Hard Bounce" 8 | }, 9 | { 10 | "EmailAddress": "asdf@softbouncemyemail.comX", 11 | "BounceType": "Soft", 12 | "Date": "2019-08-20 14:24:00", 13 | "Reason": "Soft Bounce - Dns Failure" 14 | } 15 | ], 16 | "ResultsOrderedBy": "Date", 17 | "OrderDirection": "DESC", 18 | "PageNumber": 1, 19 | "PageSize": 10, 20 | "RecordsOnThisPage": 2, 21 | "TotalNumberOfRecords": 2, 22 | "NumberOfPages": 1 23 | } 24 | -------------------------------------------------------------------------------- /test/test_verifiedhttpsconnection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from createsend.createsend import CreateSend, Unauthorized 4 | from createsend.utils import match_hostname 5 | 6 | 7 | class VerifiedHTTPSConnectionTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.cs = CreateSend({'api_key': 'not an api key'}) 11 | 12 | def test_verified_connection_no_cert(self): 13 | self.assertRaises(ValueError, match_hostname, 14 | None, 'api.createsend.com') 15 | 16 | def test_verified_connection(self): 17 | # An actual (non-stubbed) unauthenticated request to test verification. 18 | self.assertRaises(Unauthorized, self.cs.clients) 19 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install tox tox-gh 24 | - name: Test with tox 25 | run: tox -e ${{ matrix.python-version }} -------------------------------------------------------------------------------- /test/fixtures/templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "TemplateID": "5cac213cf061dd4e008de5a82b7a3621", 4 | "Name": "Template One", 5 | "PreviewURL": "https://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=01AF532CD8889B33&d=r&c=E816F55BFAD1A753", 6 | "ScreenshotURL": "https://preview.createsend.com/ts/r/14/833/263/14833263.jpg?0318092541" 7 | }, 8 | { 9 | "TemplateID": "da645c271bc85fb6550acff937c2ab2e", 10 | "Name": "Template Two", 11 | "PreviewURL": "https://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=C8A180629495E798&d=r&c=E816F55BFAD1A753", 12 | "ScreenshotURL": "https://preview.createsend.com/ts/r/18/7B3/552/187B3552.jpg?0705043527" 13 | } 14 | ] -------------------------------------------------------------------------------- /test/fixtures/segment_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "personone@example.com", 5 | "Name": "Person One", 6 | "Date": "2010-10-27 13:13:00", 7 | "ListJoinedDate": "2010-10-27 13:13:00", 8 | "State": "Active", 9 | "CustomFields": [] 10 | }, 11 | { 12 | "EmailAddress": "persontwo@example.com", 13 | "Name": "Person Two", 14 | "Date": "2010-10-27 13:13:00", 15 | "ListJoinedDate": "2010-10-27 13:13:00", 16 | "State": "Active", 17 | "CustomFields": [] 18 | } 19 | ], 20 | "ResultsOrderedBy": "email", 21 | "OrderDirection": "asc", 22 | "PageNumber": 1, 23 | "PageSize": 1000, 24 | "RecordsOnThisPage": 2, 25 | "TotalNumberOfRecords": 2, 26 | "NumberOfPages": 1 27 | } -------------------------------------------------------------------------------- /test/fixtures/campaign_bounces.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@softbouncemyemail.com", 5 | "ListID": "654523a5855b4a440bae3fb295641546", 6 | "BounceType": "Soft", 7 | "Date": "2010-07-02 16:46:00", 8 | "Reason": "Bounce - But No Email Address Returned " 9 | }, 10 | { 11 | "EmailAddress": "asdf@hardbouncemyemail.com", 12 | "ListID": "654523a5855b4a440bae3fb295641546", 13 | "BounceType": "Soft", 14 | "Date": "2010-07-02 16:46:00", 15 | "Reason": "Soft Bounce - General" 16 | } 17 | ], 18 | "ResultsOrderedBy": "date", 19 | "OrderDirection": "asc", 20 | "PageNumber": 1, 21 | "PageSize": 1000, 22 | "RecordsOnThisPage": 2, 23 | "TotalNumberOfRecords": 2, 24 | "NumberOfPages": 1 25 | } -------------------------------------------------------------------------------- /test/fixtures/journey_email_recipients_no_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@example.com", 5 | "SentDate": "2019-08-19 10:23:00" 6 | }, 7 | { 8 | "EmailAddress": "asdf+2@example.com", 9 | "SentDate": "2019-08-19 10:23:00" 10 | }, 11 | { 12 | "EmailAddress": "asdf@softbouncemyemail.comX", 13 | "SentDate": "2019-08-20 14:24:00" 14 | }, 15 | { 16 | "EmailAddress": "asdf@hardbouncemyemail.com", 17 | "SentDate": "2019-08-21 04:26:00" 18 | } 19 | ], 20 | "ResultsOrderedBy": "SentDate", 21 | "OrderDirection": "ASC", 22 | "PageNumber": 1, 23 | "PageSize": 1000, 24 | "RecordsOnThisPage": 4, 25 | "TotalNumberOfRecords": 4, 26 | "NumberOfPages": 1 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_recipients_with_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@hardbouncemyemail.com", 5 | "SentDate": "2019-08-21 04:26:00" 6 | }, 7 | { 8 | "EmailAddress": "asdf@softbouncemyemail.comX", 9 | "SentDate": "2019-08-20 14:24:00" 10 | }, 11 | { 12 | "EmailAddress": "asdf+2@example.com", 13 | "SentDate": "2019-08-19 10:23:00" 14 | }, 15 | { 16 | "EmailAddress": "asdf@example.com", 17 | "SentDate": "2019-08-19 10:23:00" 18 | } 19 | ], 20 | "ResultsOrderedBy": "SentDate", 21 | "OrderDirection": "DESC", 22 | "PageNumber": 1, 23 | "PageSize": 10, 24 | "RecordsOnThisPage": 4, 25 | "TotalNumberOfRecords": 4, 26 | "NumberOfPages": 1 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/list_stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "TotalActiveSubscribers": 6, 3 | "NewActiveSubscribersToday": 0, 4 | "NewActiveSubscribersYesterday": 8, 5 | "NewActiveSubscribersThisWeek": 8, 6 | "NewActiveSubscribersThisMonth": 8, 7 | "NewActiveSubscribersThisYear": 8, 8 | "TotalUnsubscribes": 2, 9 | "UnsubscribesToday": 0, 10 | "UnsubscribesYesterday": 2, 11 | "UnsubscribesThisWeek": 2, 12 | "UnsubscribesThisMonth": 2, 13 | "UnsubscribesThisYear": 2, 14 | "TotalDeleted": 0, 15 | "DeletedToday": 0, 16 | "DeletedYesterday": 0, 17 | "DeletedThisWeek": 0, 18 | "DeletedThisMonth": 0, 19 | "DeletedThisYear": 0, 20 | "TotalBounces": 0, 21 | "BouncesToday": 0, 22 | "BouncesYesterday": 0, 23 | "BouncesThisWeek": 0, 24 | "BouncesThisMonth": 0, 25 | "BouncesThisYear": 0 26 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines for contributing 2 | 3 | 1. [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). 4 | 2. [Create a topic branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches). 5 | 3. Make your changes, including tests for your changes which maintain [coverage](https://coveralls.io/r/campaignmonitor/createsend-python). 6 | 4. Ensure that all tests pass, by running `rake`. 7 | 5. It should go without saying, but do not increment the version number in your commits. 8 | 6. [Submit a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). 9 | -------------------------------------------------------------------------------- /test/fixtures/client_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiKey": "639d8cc27198202f5fe6037a8b17a29a59984b86d3289bc9", 3 | "AccessDetails": { 4 | "Username": "clientone", 5 | "AccessLevel": 23 6 | }, 7 | "BasicDetails": { 8 | "ClientID": "4a397ccaaa55eb4e6aa1221e1e2d7122", 9 | "CompanyName": "Client One", 10 | "ContactName": "Client One (contact)", 11 | "EmailAddress": "contact@example.com", 12 | "Country": "Australia", 13 | "TimeZone": "(GMT+10:00) Canberra, Melbourne, Sydney" 14 | }, 15 | "BillingDetails": { 16 | "CanPurchaseCredits": true, 17 | "Credits": 500, 18 | "ClientPays": true, 19 | "BaseRatePerRecipient": 1.0, 20 | "MarkupPerRecipient": 0.0, 21 | "MarkupOnDelivery": 0.0, 22 | "BaseDeliveryRate": 5.0, 23 | "Currency": "USD", 24 | "BaseDesignSpamTestRate": 5.0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/fixtures/segment_subscribers_with_tracking_preference.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "personone@example.com", 5 | "Name": "Person One", 6 | "Date": "2010-10-27 13:13:00", 7 | "ListJoinedDate": "2010-10-27 13:13:00", 8 | "State": "Active", 9 | "CustomFields": [], 10 | "ConsentToTrack": "Yes" 11 | }, 12 | { 13 | "EmailAddress": "persontwo@example.com", 14 | "Name": "Person Two", 15 | "Date": "2010-10-27 13:13:00", 16 | "ListJoinedDate": "2010-10-27 13:13:00", 17 | "State": "Active", 18 | "CustomFields": [], 19 | "ConsentToTrack": "No" 20 | } 21 | ], 22 | "ResultsOrderedBy": "email", 23 | "OrderDirection": "asc", 24 | "PageNumber": 1, 25 | "PageSize": 1000, 26 | "RecordsOnThisPage": 2, 27 | "TotalNumberOfRecords": 2, 28 | "NumberOfPages": 1 29 | } -------------------------------------------------------------------------------- /test/fixtures/tx_smartemail_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "SmartEmailID": "bb4a6ebb-663d-42a0-bdbe-60512cf30a01", 3 | "Name": "Reset Password", 4 | "CreatedAt": "2015-03-31T16:06:35+11:00", 5 | "Status": "active", 6 | "Properties": { 7 | "From": "\"Team\" ", 8 | "ReplyTo": "joe@example.com", 9 | "Subject": "[username], your password has been reset!", 10 | "Content": { 11 | "HTML": "Content managed in Email Builder", 12 | "Text": "Content managed in Email Builder", 13 | "EmailVariables": [ 14 | "username", 15 | "reset_token" 16 | ], 17 | "InlineCss": true 18 | }, 19 | "TextPreviewUrl": "https://philoye.devcreatesend.com/t/r-9A7A28EE76054977/T", 20 | "HtmlPreviewUrl": "https://philoye.devcreatesend.com/t/r-9A7A28EE76054977/" 21 | }, 22 | "AddRecipientsToList": true 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/drafts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "CampaignID": "7c7424792065d92627139208c8c01db1", 4 | "Name": "Draft One", 5 | "Subject": "Draft One", 6 | "FromName": "My Name", 7 | "FromEmail": "myemail@example.com", 8 | "ReplyTo": "myemail@example.com", 9 | "DateCreated": "2010-08-19 16:08:00", 10 | "PreviewURL": "https://createsend.com/t/r-E97A7BB2E6983DA1", 11 | "PreviewTextURL": "https://createsend.com/t/r-E97A7BB2E6983DA1/t", 12 | "Tags": ["Tags5"] 13 | }, 14 | { 15 | "CampaignID": "2e928e982065d92627139208c8c01db1", 16 | "Name": "Draft Two", 17 | "Subject": "Draft Two", 18 | "FromName": "My Name", 19 | "FromEmail": "myemail@example.com", 20 | "ReplyTo": "myemail@example.com", 21 | "DateCreated": "2010-08-19 16:08:00", 22 | "PreviewURL": "https://createsend.com/t/r-E97A7BB2E6983DA1", 23 | "PreviewTextURL": "https://createsend.com/t/r-E97A7BB2E6983DA1/t", 24 | "Tags": [] 25 | } 26 | ] -------------------------------------------------------------------------------- /test/fixtures/email_client_usage.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Client": "iOS Devices", 4 | "Version": "iPhone", 5 | "Percentage": 19.83, 6 | "Subscribers": 7056 7 | }, 8 | { 9 | "Client": "Apple Mail", 10 | "Version": "Apple Mail 6", 11 | "Percentage": 13.02, 12 | "Subscribers": 4633 13 | }, 14 | { 15 | "Client": "Apple Mail", 16 | "Version": "Apple Mail 5", 17 | "Percentage": 10.60, 18 | "Subscribers": 3773 19 | }, 20 | { 21 | "Client": "Microsoft Outlook", 22 | "Version": "Outlook 2010", 23 | "Percentage": 7.18, 24 | "Subscribers": 2556 25 | }, 26 | { 27 | "Client": "iOS Devices", 28 | "Version": "iPad", 29 | "Percentage": 4.43, 30 | "Subscribers": 1577 31 | }, 32 | { 33 | "Client": "Undetectable", 34 | "Version": "Undetectable", 35 | "Percentage": 4.94, 36 | "Subscribers": 1632 37 | } 38 | ] -------------------------------------------------------------------------------- /test/fixtures/journey_email_opens_no_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@example.com", 5 | "Date": "2019-08-19 10:23:00", 6 | "IPAddress": "198.148.196.144", 7 | "Latitude": -33.8591, 8 | "Longitude": 151.200195, 9 | "City": "Sydney", 10 | "Region": "New South Wales", 11 | "CountryCode": "AU", 12 | "CountryName": "Australia" 13 | }, 14 | { 15 | "EmailAddress": "asdf+2@example.com", 16 | "Date": "2019-08-19 10:24:00", 17 | "IPAddress": "198.148.196.144", 18 | "Latitude": -33.8591, 19 | "Longitude": 151.200195, 20 | "City": "Sydney", 21 | "Region": "New South Wales", 22 | "CountryCode": "AU", 23 | "CountryName": "Australia" 24 | } 25 | ], 26 | "ResultsOrderedBy": "Date", 27 | "OrderDirection": "ASC", 28 | "PageNumber": 1, 29 | "PageSize": 1000, 30 | "RecordsOnThisPage": 2, 31 | "TotalNumberOfRecords": 2, 32 | "NumberOfPages": 1 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_opens_with_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf+2@example.com", 5 | "Date": "2019-08-19 10:24:00", 6 | "IPAddress": "198.148.196.144", 7 | "Latitude": -33.8591, 8 | "Longitude": 151.200195, 9 | "City": "Sydney", 10 | "Region": "New South Wales", 11 | "CountryCode": "AU", 12 | "CountryName": "Australia" 13 | }, 14 | { 15 | "EmailAddress": "asdf@example.com", 16 | "Date": "2019-08-19 10:23:00", 17 | "IPAddress": "198.148.196.144", 18 | "Latitude": -33.8591, 19 | "Longitude": 151.200195, 20 | "City": "Sydney", 21 | "Region": "New South Wales", 22 | "CountryCode": "AU", 23 | "CountryName": "Australia" 24 | } 25 | ], 26 | "ResultsOrderedBy": "Date", 27 | "OrderDirection": "DESC", 28 | "PageNumber": 1, 29 | "PageSize": 10, 30 | "RecordsOnThisPage": 2, 31 | "TotalNumberOfRecords": 2, 32 | "NumberOfPages": 1 33 | } 34 | -------------------------------------------------------------------------------- /lib/createsend/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'createsend-python' 2 | __author__ = 'Campaign Monitor' 3 | __license__ = 'MIT' 4 | __copyright__ = 'Copyright 2017' 5 | 6 | 7 | from createsend.utils import ( 8 | CertificateError, 9 | VerifiedHTTPSConnection, 10 | json_to_py, 11 | dict_to_object, 12 | get_faker) 13 | from createsend.administrator import Administrator 14 | from createsend.campaign import Campaign 15 | from createsend.client import Client 16 | from createsend.createsend import ( 17 | CreateSendError, 18 | ClientError, 19 | ServerError, 20 | BadRequest, 21 | Unauthorized, 22 | NotFound, 23 | Unavailable, 24 | ExpiredOAuthToken, 25 | CreateSendBase, 26 | CreateSend) 27 | from createsend.journey_email import JourneyEmail 28 | from createsend.journey import Journey 29 | from createsend.list import List 30 | from createsend.person import Person 31 | from createsend.segment import Segment 32 | from createsend.subscriber import Subscriber 33 | from createsend.template import Template 34 | from createsend.transactional import Transactional 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2024 Campaign Monitor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /samples/lists.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | listId = 'YOUR_LIST_ID' 7 | 8 | list = List(auth, listId) 9 | 10 | # Get list of active subscribers in a list 11 | print("List of active subscribers:") 12 | for cm in list.active().Results: 13 | print(" - %s" % cm.EmailAddress) 14 | 15 | # Get list of bounced subscribers in a list 16 | print("List of bounced subscribers:") 17 | for cm in list.bounced().Results: 18 | print(" - %s" % cm.EmailAddress) 19 | 20 | # Get list of unconfirmed subscribers in a list 21 | print("List of unconfirmed subscribers:") 22 | for cm in list.unconfirmed().Results: 23 | print(" - %s" % cm.EmailAddress) 24 | 25 | # Get list of unsubscribed subscribers in a list 26 | print("List of unsubscribed subscribers:") 27 | for cm in list.unsubscribed().Results: 28 | print(" - %s" % cm.EmailAddress) 29 | 30 | # Get list of deleted subscribers in a list 31 | print("List of deleted subscribers:") 32 | for cm in list.deleted().Results: 33 | print(" - %s" % cm.EmailAddress) -------------------------------------------------------------------------------- /test/fixtures/journey_email_clicks_no_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf@example.com", 5 | "Date": "2019-08-19 10:23:00", 6 | "URL": "https://mail.google.com/mail/?hl=en&tab=wm", 7 | "IPAddress": "198.148.196.144", 8 | "Latitude": -33.8591, 9 | "Longitude": 151.200195, 10 | "City": "Sydney", 11 | "Region": "New South Wales", 12 | "CountryCode": "AU", 13 | "CountryName": "Australia" 14 | }, 15 | { 16 | "EmailAddress": "asdf+2@example.com", 17 | "Date": "2019-08-19 10:24:00", 18 | "URL": "https://example.com", 19 | "IPAddress": "198.148.196.144", 20 | "Latitude": -33.8591, 21 | "Longitude": 151.200195, 22 | "City": "Sydney", 23 | "Region": "New South Wales", 24 | "CountryCode": "AU", 25 | "CountryName": "Australia" 26 | } 27 | ], 28 | "ResultsOrderedBy": "Date", 29 | "OrderDirection": "ASC", 30 | "PageNumber": 1, 31 | "PageSize": 1000, 32 | "RecordsOnThisPage": 2, 33 | "TotalNumberOfRecords": 2, 34 | "NumberOfPages": 1 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/journey_email_clicks_with_params.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "asdf+2@example.com", 5 | "Date": "2019-08-19 10:24:00", 6 | "URL": "https://example.com", 7 | "IPAddress": "198.148.196.144", 8 | "Latitude": -33.8591, 9 | "Longitude": 151.200195, 10 | "City": "Sydney", 11 | "Region": "New South Wales", 12 | "CountryCode": "AU", 13 | "CountryName": "Australia" 14 | }, 15 | { 16 | "EmailAddress": "asdf@example.com", 17 | "Date": "2019-08-19 10:23:00", 18 | "URL": "https://mail.google.com/mail/?hl=en&tab=wm", 19 | "IPAddress": "198.148.196.144", 20 | "Latitude": -33.8591, 21 | "Longitude": 151.200195, 22 | "City": "Sydney", 23 | "Region": "New South Wales", 24 | "CountryCode": "AU", 25 | "CountryName": "Australia" 26 | } 27 | ], 28 | "ResultsOrderedBy": "Date", 29 | "OrderDirection": "DESC", 30 | "PageNumber": 1, 31 | "PageSize": 10, 32 | "RecordsOnThisPage": 2, 33 | "TotalNumberOfRecords": 2, 34 | "NumberOfPages": 1 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/tx_message_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 3 | "Status": "Delivered", 4 | "SentAt": "2014-01-15T16:09:19-05:00", 5 | "SmartEmailID": "c0da9c4c-e7e4-11e4-a74d-6c4008bc7468", 6 | "CanBeResent": true, 7 | "Recipient": "Joe Smith ", 8 | "Message": { 9 | "From": "Team Webapp ", 13 | "jamesmith@example.com" 14 | ], 15 | "CC": [ 16 | "Joe Smith " 17 | ], 18 | "BCC": "joesmith@example.com", 19 | "Attachments": [ 20 | { 21 | "Name": "Invoice.pdf", 22 | "Type": "application/pdf" 23 | } 24 | ], 25 | "Body": { 26 | "Html": "...", 27 | "Text": "..." 28 | }, 29 | "Data": { 30 | "new_password_url": "https://www.mywebapp.com/newpassword?uid=jguf45hf74hbf74gf" 31 | } 32 | }, 33 | "TotalOpens": 1, 34 | "TotalClicks": 1 35 | } 36 | 37 | -------------------------------------------------------------------------------- /test/fixtures/scheduled_campaigns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "DateScheduled": "2011-05-25 10:40:00", 4 | "ScheduledTimeZone": "(GMT+10:00) Canberra, Melbourne, Sydney", 5 | "CampaignID": "827dbbd2161ea9989fa11ad562c66937", 6 | "Name": "Magic Issue One", 7 | "Subject": "Magic Issue One", 8 | "FromName": "My Name", 9 | "FromEmail": "myemail@example.com", 10 | "ReplyTo": "myemail@example.com", 11 | "DateCreated": "2011-05-24 10:37:00", 12 | "PreviewURL": "https://createsend.com/t/r-DD543521A87C9B8B", 13 | "PreviewTextURL": "https://createsend.com/t/r-DD543521A87C9B8B/t", 14 | "Tags": [] 15 | }, 16 | { 17 | "DateScheduled": "2011-05-29 11:20:00", 18 | "ScheduledTimeZone": "(GMT+10:00) Canberra, Melbourne, Sydney", 19 | "CampaignID": "4f54bbd2161e65789fa11ad562c66937", 20 | "Name": "Magic Issue Two", 21 | "Subject": "Magic Issue Two", 22 | "FromName": "My Name", 23 | "FromEmail": "myemail@example.com", 24 | "ReplyTo": "myemail@example.com", 25 | "DateCreated": "2011-05-24 10:39:00", 26 | "PreviewURL": "https://createsend.com/t/r-DD913521A87C9B8B", 27 | "PreviewTextURL": "https://createsend.com/t/r-DD913521A87C9B8B/t", 28 | "Tags": ["Tags3", "Tags4"] 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/suppressionlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "SuppressionReason": "Unsubscribed", 5 | "EmailAddress": "example+1@example.com", 6 | "Date": "2010-10-26 10:55:31", 7 | "State": "Suppressed" 8 | }, 9 | { 10 | "SuppressionReason": "Unsubscribed", 11 | "EmailAddress": "example+2@example.com", 12 | "Date": "2010-10-26 10:55:31", 13 | "State": "Suppressed" 14 | }, 15 | { 16 | "SuppressionReason": "Unsubscribed", 17 | "EmailAddress": "example+3@example.com", 18 | "Date": "2010-10-26 10:55:31", 19 | "State": "Suppressed" 20 | }, 21 | { 22 | "SuppressionReason": "Unsubscribed", 23 | "EmailAddress": "subscriber@example.com", 24 | "Date": "2010-10-25 13:11:04", 25 | "State": "Suppressed" 26 | }, 27 | { 28 | "SuppressionReason": "Unsubscribed", 29 | "EmailAddress": "subscriberone@example.com", 30 | "Date": "2010-10-25 13:04:15", 31 | "State": "Suppressed" 32 | } 33 | ], 34 | "ResultsOrderedBy": "email", 35 | "OrderDirection": "asc", 36 | "PageNumber": 1, 37 | "PageSize": 1000, 38 | "RecordsOnThisPage": 5, 39 | "TotalNumberOfRecords": 5, 40 | "NumberOfPages": 1 41 | } -------------------------------------------------------------------------------- /test/fixtures/subscriber_history.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ID": "fc0ce7105baeaf97f47c99be31d02a91", 4 | "Type": "Campaign", 5 | "Name": "Campaign One", 6 | "Actions": [ 7 | { 8 | "Event": "Open", 9 | "Date": "2010-10-12 13:18:00", 10 | "IPAddress": "192.168.126.87", 11 | "Detail": "" 12 | }, 13 | { 14 | "Event": "Click", 15 | "Date": "2010-10-12 13:16:00", 16 | "IPAddress": "192.168.126.87", 17 | "Detail": "https://example.com/post/12323/" 18 | }, 19 | { 20 | "Event": "Click", 21 | "Date": "2010-10-12 13:15:00", 22 | "IPAddress": "192.168.126.87", 23 | "Detail": "https://example.com/post/29889/" 24 | }, 25 | { 26 | "Event": "Open", 27 | "Date": "2010-10-12 13:15:00", 28 | "IPAddress": "192.168.126.87", 29 | "Detail": "" 30 | }, 31 | { 32 | "Event": "Click", 33 | "Date": "2010-10-12 13:01:00", 34 | "IPAddress": "192.168.126.87", 35 | "Detail": "https://example.com/post/82211/" 36 | }, 37 | { 38 | "Event": "Open", 39 | "Date": "2010-10-12 13:01:00", 40 | "IPAddress": "192.168.126.87", 41 | "Detail": "" 42 | } 43 | ] 44 | } 45 | ] -------------------------------------------------------------------------------- /test/fixtures/tx_messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 4 | "Status": "Delivered", 5 | "SentAt": "2014-01-15T16:09:19-05:00", 6 | "Recipient": "Joe Smith ", 7 | "From": "Team ", 8 | "Subject": "Ungrouped message", 9 | "TotalOpens": 2, 10 | "TotalClicks": 4, 11 | "CanBeResent": true 12 | }, 13 | { 14 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 15 | "Status": "Delivered", 16 | "SentAt": "2014-01-15T16:09:19-05:00", 17 | "Recipient": "Joe Smith ", 18 | "From": "Team ", 19 | "Subject": "Your password has been reset", 20 | "TotalOpens": 2, 21 | "TotalClicks": 4, 22 | "CanBeResent": true, 23 | "Group": "Password Reset" 24 | }, 25 | { 26 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 27 | "Status": "Bounced", 28 | "SentAt": "2014-01-15T16:09:19-05:00", 29 | "Recipient": "Joe Smith ", 30 | "From": "Team ", 31 | "Subject": "Your credit card has expired", 32 | "TotalOpens": 2, 33 | "TotalClicks": 4, 34 | "CanBeResent": true, 35 | "SmartEmailID": "21dab350-f484-11e4-ad38-6c4008bc7468" 36 | } 37 | ] 38 | 39 | -------------------------------------------------------------------------------- /test/fixtures/unconfirmed_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+7t8787Y@example.com", 5 | "Name": "Unconfirmed One", 6 | "Date": "2010-10-25 10:28:00", 7 | "ListJoinedDate": "2010-10-25 10:28:00", 8 | "State": "Unconfirmed", 9 | "CustomFields": [ 10 | { 11 | "Key": "website", 12 | "Value": "https://example.com" 13 | } 14 | ], 15 | "ReadsEmailWith": "" 16 | }, 17 | { 18 | "EmailAddress": "subs+7878787y8ggg@example.com", 19 | "Name": "Unconfirmed Two", 20 | "Date": "2010-10-25 12:17:00", 21 | "ListJoinedDate": "2010-10-25 12:17:00", 22 | "State": "Unconfirmed", 23 | "CustomFields": [ 24 | { 25 | "Key": "website", 26 | "Value": "https://subdomain.example.com" 27 | } 28 | ], 29 | "ReadsEmailWith": "" 30 | } 31 | ], 32 | "ResultsOrderedBy": "email", 33 | "OrderDirection": "asc", 34 | "PageNumber": 1, 35 | "PageSize": 1000, 36 | "RecordsOnThisPage": 2, 37 | "TotalNumberOfRecords": 2, 38 | "NumberOfPages": 1 39 | } -------------------------------------------------------------------------------- /test/fixtures/campaigns.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "WebVersionURL": "https://createsend.com/t/r-765E86829575EE2C", 5 | "WebVersionTextURL": "https://createsend.com/t/r-765E86829575EE2C/t", 6 | "CampaignID": "fc0ce7105baeaf97f47c99be31d02a91", 7 | "Subject": "Campaign One", 8 | "Name": "Campaign One", 9 | "FromName": "My Name", 10 | "FromEmail": "myemail@example.com", 11 | "ReplyTo": "myemail@example.com", 12 | "SentDate": "2010-10-12 12:58:00", 13 | "TotalRecipients": 2245, 14 | "Tags": ["Tag1", "Tag2"] 15 | }, 16 | { 17 | "WebVersionURL": "https://createsend.com/t/r-DD543566A87C9B8B", 18 | "WebVersionTextURL": "https://createsend.com/t/r-DD543566A87C9B8B/t", 19 | "CampaignID": "072472b88c853ae5dedaeaf549a8d607", 20 | "Subject": "Campaign Two", 21 | "Name": "Campaign Two", 22 | "FromName": "My Name", 23 | "FromEmail": "myemail@example.com", 24 | "ReplyTo": "myemail@example.com", 25 | "SentDate": "2010-10-06 16:20:00", 26 | "TotalRecipients": 11222 27 | } 28 | ], 29 | "ResultsOrderedBy": "SentDate", 30 | "OrderDirection": "desc", 31 | "PageNumber": 1, 32 | "PageSize": 2, 33 | "RecordsOnThisPage": 2, 34 | "TotalNumberOfRecords": 49, 35 | "NumberOfPages": 25 36 | } -------------------------------------------------------------------------------- /lib/createsend/template.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Template(CreateSendBase): 8 | """Represents an email template and associated functionality.""" 9 | 10 | def __init__(self, auth=None, template_id=None): 11 | self.template_id = template_id 12 | super().__init__(auth) 13 | 14 | def create(self, client_id, name, html_url, zip_url): 15 | """Creates a new email template.""" 16 | body = { 17 | "Name": name, 18 | "HtmlPageURL": html_url, 19 | "ZipFileURL": zip_url} 20 | response = self._post("/templates/%s.json" % 21 | client_id, json.dumps(body)) 22 | self.template_id = json_to_py(response) 23 | return self.template_id 24 | 25 | def details(self): 26 | """Gets the details of this email template.""" 27 | response = self._get("/templates/%s.json" % self.template_id) 28 | return json_to_py(response) 29 | 30 | def update(self, name, html_url, zip_url): 31 | """Updates this email template.""" 32 | body = { 33 | "Name": name, 34 | "HtmlPageURL": html_url, 35 | "ZipFileURL": zip_url} 36 | response = self._put("/templates/%s.json" % 37 | self.template_id, json.dumps(body)) 38 | 39 | def delete(self): 40 | """Deletes this email template.""" 41 | response = self._delete("/templates/%s.json" % self.template_id) 42 | -------------------------------------------------------------------------------- /test/fixtures/campaign_clicks.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+6576576576@example.com", 5 | "URL": "https://video.google.com.au/?hl=en&tab=wv", 6 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 7 | "Date": "2010-10-11 08:29:00", 8 | "IPAddress": "192.168.126.87", 9 | "Latitude": -33.8683, 10 | "Longitude": 151.2086, 11 | "City": "Sydney", 12 | "Region": "New South Wales", 13 | "CountryCode": "AU", 14 | "CountryName": "Australia" 15 | }, 16 | { 17 | "EmailAddress": "subs+6576576576@example.com", 18 | "URL": "https://mail.google.com/mail/?hl=en&tab=wm", 19 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 20 | "Date": "2010-10-11 08:29:00", 21 | "IPAddress": "192.168.126.87", 22 | "Latitude": -33.8683, 23 | "Longitude": 151.2086, 24 | "City": "Sydney", 25 | "Region": "New South Wales", 26 | "CountryCode": "AU", 27 | "CountryName": "Australia" 28 | }, 29 | { 30 | "EmailAddress": "subs+6576576576@example.com", 31 | "URL": "https://mail.google.com/mail/?hl=en&tab=wm", 32 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 33 | "Date": "2010-10-06 17:24:00", 34 | "IPAddress": "192.168.126.87", 35 | "Latitude": -33.8683, 36 | "Longitude": 151.2086, 37 | "City": "Sydney", 38 | "Region": "New South Wales", 39 | "CountryCode": "AU", 40 | "CountryName": "Australia" 41 | } 42 | ], 43 | "ResultsOrderedBy": "date", 44 | "OrderDirection": "asc", 45 | "PageNumber": 1, 46 | "PageSize": 1000, 47 | "RecordsOnThisPage": 3, 48 | "TotalNumberOfRecords": 3, 49 | "NumberOfPages": 1 50 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="createsend", 5 | version='9.1.4', 6 | description="A library which implements the complete functionality of the Campaign Monitor API.", 7 | author='Campaign Monitor', 8 | author_email='support@campaignmonitor.com', 9 | url="https://campaignmonitor.github.io/createsend-python/", 10 | license="MIT", 11 | keywords="createsend campaign monitor email", 12 | packages=find_packages('lib'), 13 | package_dir={'': 'lib'}, 14 | package_data={'': ['cacert.pem']}, 15 | 16 | classifiers=[ 17 | "Development Status :: 5 - Production/Stable", 18 | 19 | # Who and what the project is for 20 | "Intended Audience :: Developers", 21 | "Topic :: Communications", 22 | "Topic :: Communications :: Email", 23 | "Topic :: Communications :: Email :: Mailing List Servers", 24 | "Topic :: Internet", 25 | "Topic :: Software Development :: Libraries", 26 | 27 | # License classifiers 28 | "License :: OSI Approved :: MIT License", 29 | "License :: DFSG approved", 30 | "License :: OSI Approved", 31 | 32 | # Generally, we support the following. 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3", 35 | 36 | # Specifically, we support the following releases. 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3.12", 42 | "Programming Language :: Python :: 3.13", 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /lib/createsend/administrator.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Administrator(CreateSendBase): 8 | """Represents an administrator and associated functionality.""" 9 | 10 | def __init__(self, auth=None, email_address=None): 11 | self.email_address = email_address 12 | super().__init__(auth) 13 | 14 | def get(self, email_address=None): 15 | """Gets an administrator by email address.""" 16 | params = {"email": email_address or self.email_address} 17 | response = self._get("/admins.json", params=params) 18 | return json_to_py(response) 19 | 20 | def add(self, email_address, name): 21 | """Adds an administrator to an account.""" 22 | body = { 23 | "EmailAddress": email_address, 24 | "Name": name} 25 | response = self._post("/admins.json", json.dumps(body)) 26 | return json_to_py(response) 27 | 28 | def update(self, new_email_address, name): 29 | """Updates the details for an administrator.""" 30 | params = {"email": self.email_address} 31 | body = { 32 | "EmailAddress": new_email_address, 33 | "Name": name} 34 | response = self._put("/admins.json", 35 | body=json.dumps(body), params=params) 36 | # Update self.email_address, so this object can continue to be used 37 | # reliably 38 | self.email_address = new_email_address 39 | 40 | def delete(self): 41 | """Deletes the administrator from the account.""" 42 | params = {"email": self.email_address} 43 | response = self._delete("/admins.json", params=params) 44 | -------------------------------------------------------------------------------- /test/test_journey.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from createsend.journey import Journey 4 | 5 | 6 | class JourneyTestCase: 7 | 8 | def test_summary(self): 9 | self.journey.stub_request( 10 | "journeys/%s.json" % self.journey_id, "journey_summary.json") 11 | summary = self.journey.summary() 12 | self.assertEqual(summary.JourneyID, "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1") 13 | self.assertEqual(summary.Name, "New journey") 14 | self.assertEqual(summary.TriggerType, "On Subscription") 15 | self.assertEqual(summary.Status, "Active") 16 | email_one = summary.Emails[0] 17 | self.assertEqual(email_one.EmailID, "b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1") 18 | self.assertEqual(email_one.Name, "1") 19 | self.assertEqual(email_one.Bounced, 7) 20 | self.assertEqual(email_one.Clicked, 1) 21 | self.assertEqual(email_one.Opened, 12) 22 | self.assertEqual(email_one.Sent, 11) 23 | self.assertEqual(email_one.UniqueOpened, 4) 24 | self.assertEqual(email_one.Unsubscribed, 1) 25 | 26 | 27 | class OAuthJourneyTestCase(unittest.TestCase, JourneyTestCase): 28 | """Test when using OAuth to authenticate""" 29 | 30 | def setUp(self): 31 | self.journey_id = "787y87y87y87y87y87y87" 32 | self.journey = Journey( 33 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, self.journey_id) 34 | 35 | 36 | class ApiKeyJourneyTestCase(unittest.TestCase, JourneyTestCase): 37 | """Test when using an API key to authenticate""" 38 | 39 | def setUp(self): 40 | self.journey_id = "787y87y87y87y87y87y87" 41 | self.journey = Journey( 42 | {'api_key': '123123123123123123123'}, self.journey_id) 43 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing createsend-python 2 | 3 | ## Requirements 4 | 5 | - You must have a [PyPI](https://pypi.python.org/pypi) account and must be an owner or maintainer of the [createsend](https://pypi.python.org/pypi/createsend/) package. 6 | - You must install [Twine](https://pypi.org/project/twine/) 7 | ``` 8 | pip install twine 9 | ``` 10 | - You must install [setuptools](https://pypi.org/project/setuptools/) 11 | ``` 12 | pip install setuptools 13 | ``` 14 | 15 | ## Prepare the release 16 | 17 | - Increment `version` in the `setup.py` file, ensuring that you use [Semantic Versioning](https://semver.org/). 18 | - Add an entry to `HISTORY.md` which clearly explains the new release. 19 | - Commit your changes: 20 | 21 | ``` 22 | git commit -am "Version X.Y.Z" 23 | ``` 24 | 25 | - Tag the new version: 26 | 27 | ``` 28 | git tag -a vX.Y.Z -m "Version X.Y.Z" 29 | ``` 30 | 31 | - Push your changes to GitHub, including the tag you just created: 32 | 33 | ``` 34 | git push origin master --tags 35 | ``` 36 | 37 | - Ensure that all [tests](https://github.com/campaignmonitor/createsend-python/actions/workflows/tests.yml) pass, and that [coverage](https://coveralls.io/r/campaignmonitor/createsend-python) is maintained or improved. 38 | 39 | - Add a new [GitHub Release](https://github.com/campaignmonitor/createsend-python/releases) using the newly created tag. 40 | 41 | ## Build the package 42 | 43 | ``` 44 | rake build 45 | ``` 46 | 47 | This builds a source distribution of the package locally to a file named something like `dist/createsend-X.Y.Z.tar.gz`. You're now ready to release the package. 48 | 49 | ## Release the package 50 | 51 | ``` 52 | rake release 53 | ``` 54 | 55 | This publishes the package to [PyPI](https://pypi.python.org/pypi/createsend/). You should see the newly published version of the package there. All done! 56 | -------------------------------------------------------------------------------- /test/fixtures/deleted_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subscriber@example.com", 5 | "Name": "Deleted One", 6 | "Date": "2010-10-25 13:11:00", 7 | "ListJoinedDate": "2010-10-25 13:11:00", 8 | "State": "Deleted", 9 | "CustomFields": [], 10 | "ReadsEmailWith": "Gmail" 11 | }, 12 | { 13 | "EmailAddress": "subscriberone@example.com", 14 | "Name": "Subscriber", 15 | "Date": "2010-10-25 13:04:00", 16 | "ListJoinedDate": "2010-10-25 13:04:00", 17 | "State": "Deleted", 18 | "CustomFields": [ 19 | { 20 | "Key": "website", 21 | "Value": "https://google.com" 22 | } 23 | ], 24 | "ReadsEmailWith": "Gmail" 25 | }, 26 | { 27 | "EmailAddress": "example+1@example.com", 28 | "Name": "Example One", 29 | "Date": "2010-10-26 10:56:00", 30 | "ListJoinedDate": "2010-10-26 10:56:00", 31 | "State": "Deleted", 32 | "CustomFields": [], 33 | "ReadsEmailWith": "" 34 | }, 35 | { 36 | "EmailAddress": "example+2@example.com", 37 | "Name": "Example Two", 38 | "Date": "2010-10-26 10:56:00", 39 | "ListJoinedDate": "2010-10-26 10:56:00", 40 | "State": "Deleted", 41 | "CustomFields": [], 42 | "ReadsEmailWith": "" 43 | }, 44 | { 45 | "EmailAddress": "example+3@example.com", 46 | "Name": "Example Three", 47 | "Date": "2010-10-26 10:56:00", 48 | "ListJoinedDate": "2010-10-26 10:56:00", 49 | "State": "Deleted", 50 | "CustomFields": [], 51 | "ReadsEmailWith": "Gmail" 52 | } 53 | ], 54 | "ResultsOrderedBy": "email", 55 | "OrderDirection": "asc", 56 | "PageNumber": 1, 57 | "PageSize": 1000, 58 | "RecordsOnThisPage": 5, 59 | "TotalNumberOfRecords": 5, 60 | "NumberOfPages": 1 61 | } -------------------------------------------------------------------------------- /samples/subscribers.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | listId = 'YOUR_LIST_ID' 7 | emailAddress = 'YOUR_SUBSCRIBER_EMAIL_ADDRESS' 8 | 9 | subscriberName = "YOUR_SUBSCRIBER_NAME" 10 | subscriberCustomFields = [] 11 | subscriberResubscribed = False 12 | subscriberConsentToTrack = 'Unchanged' 13 | subscriberMobileNumber = "+61491570006" # This is a reserved mobile number by the Australian Communications and Media Authority 14 | subscriberConsentToSendSms = "Yes" 15 | 16 | subscriber = Subscriber(auth, listId, emailAddress) 17 | 18 | # Get the details for a subscriber 19 | subscriberDetail = subscriber.get() 20 | for property, value in vars(subscriberDetail).items(): 21 | print(property, ":", value) 22 | 23 | # Adding a subscriber 24 | #This implemntation defaults the value of 'restart_subscription_based_autoresponders' to false 25 | subscriber.add(listId, emailAddress, subscriberName, subscriberCustomFields, subscriberResubscribed, subscriberConsentToTrack) 26 | 27 | # Adding a subscriber with a mobile number 28 | #This implemntation defaults the value of 'restart_subscription_based_autoresponders' to false 29 | #This also sets the default value of 'consent_to_track_sms' to 'unchanged', meaning new users will not receive SMS communications by default." 30 | subscriber.add(listId, emailAddress, subscriberName, subscriberCustomFields, subscriberConsentToTrack, mobile_Number=subscriberMobileNumber) 31 | 32 | #Alternative to set SMS tracking permissions 33 | # This implemntation defaults the value of 'restart_subscription_based_autoresponders' to false 34 | subscriber.add(listId, emailAddress, subscriberName, subscriberCustomFields, subscriberConsentToTrack, mobile_Number=subscriberMobileNumber, consent_to_track_sms=subscriberConsentToSendSms) -------------------------------------------------------------------------------- /test/fixtures/unsubscribed_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subscriber@example.com", 5 | "Name": "Unsub One", 6 | "Date": "2010-10-25 13:11:00", 7 | "ListJoinedDate": "2010-10-25 13:11:00", 8 | "State": "Unsubscribed", 9 | "CustomFields": [], 10 | "ReadsEmailWith": "Gmail" 11 | }, 12 | { 13 | "EmailAddress": "subscriberone@example.com", 14 | "Name": "Subscriber", 15 | "Date": "2010-10-25 13:04:00", 16 | "ListJoinedDate": "2010-10-25 13:04:00", 17 | "State": "Unsubscribed", 18 | "CustomFields": [ 19 | { 20 | "Key": "website", 21 | "Value": "https://google.com" 22 | } 23 | ], 24 | "ReadsEmailWith": "Gmail" 25 | }, 26 | { 27 | "EmailAddress": "example+1@example.com", 28 | "Name": "Example One", 29 | "Date": "2010-10-26 10:56:00", 30 | "ListJoinedDate": "2010-10-26 10:56:00", 31 | "State": "Unsubscribed", 32 | "CustomFields": [], 33 | "ReadsEmailWith": "" 34 | }, 35 | { 36 | "EmailAddress": "example+2@example.com", 37 | "Name": "Example Two", 38 | "Date": "2010-10-26 10:56:00", 39 | "ListJoinedDate": "2010-10-26 10:56:00", 40 | "State": "Unsubscribed", 41 | "CustomFields": [], 42 | "ReadsEmailWith": "" 43 | }, 44 | { 45 | "EmailAddress": "example+3@example.com", 46 | "Name": "Example Three", 47 | "Date": "2010-10-26 10:56:00", 48 | "ListJoinedDate": "2010-10-26 10:56:00", 49 | "State": "Unsubscribed", 50 | "CustomFields": [], 51 | "ReadsEmailWith": "Gmail" 52 | } 53 | ], 54 | "ResultsOrderedBy": "email", 55 | "OrderDirection": "asc", 56 | "PageNumber": 1, 57 | "PageSize": 1000, 58 | "RecordsOnThisPage": 5, 59 | "TotalNumberOfRecords": 5, 60 | "NumberOfPages": 1 61 | } -------------------------------------------------------------------------------- /samples/clients.py: -------------------------------------------------------------------------------- 1 | from createsend import * 2 | 3 | auth = { 4 | 'access_token': 'YOUR_ACCESS_TOKEN', 5 | 'refresh_token': 'YOUR_REFRESH_TOKEN' } 6 | clientId = 'YOUR_CLIENT_ID' 7 | 8 | 9 | cs = CreateSend(auth) 10 | client = Client(auth, clientId) 11 | 12 | # Get list of sent campaigns 13 | print("List of sent campaigns:") 14 | pageNumber = 1 15 | pagedCampaigns = client.campaigns(page = 1) 16 | numberOfPages = pagedCampaigns.NumberOfPages 17 | while pageNumber <= numberOfPages: 18 | if (pageNumber > 1): 19 | pagedCampaigns = client.campaigns(page = pageNumber) 20 | 21 | print(" Page: %d" % pageNumber) 22 | for cm in pagedCampaigns.Results: 23 | print(" - %s" % cm.Subject) 24 | 25 | pageNumber = pageNumber + 1 26 | 27 | 28 | # Get list of sent campaigns filtered by tags and date 29 | print("List of sent campaigns at 2021 with ABTest tag:") 30 | pageNumber = 1 31 | pagedCampaigns = client.campaigns(page = 1, sent_from_date="2021-01-01", sent_to_date="2022-01-01", tags="ABTest") 32 | numberOfPages = pagedCampaigns.NumberOfPages 33 | while pageNumber <= numberOfPages: 34 | if (pageNumber > 1): 35 | pagedCampaigns = client.campaigns(page = pageNumber, sent_from_date="2021-01-01", sent_to_date="2022-01-01", tags="ABTest") 36 | 37 | print(" Page: %d" % pageNumber) 38 | for cm in pagedCampaigns.Results: 39 | print(" - %s" % cm.Subject) 40 | 41 | pageNumber = pageNumber + 1 42 | 43 | # Get list of drafts campaigns 44 | print("List of drafts campaigns:") 45 | for cm in client.drafts(): 46 | print(" - %s" % cm.Subject) 47 | 48 | # Get list of scheduled campaigns 49 | print("List of scheduled campaigns:") 50 | for cm in client.scheduled(): 51 | print(" - %s" % cm.Subject) 52 | 53 | # Get list of tags 54 | print("List of tags:") 55 | for tag in client.tags(): 56 | print(" Tag: %s - NumberOfCampaigns: %d" % (tag.Name, tag.NumberOfCampaigns)) -------------------------------------------------------------------------------- /test/fixtures/campaign_opens.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+6576576576@example.com", 5 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 6 | "Date": "2010-10-11 08:29:00", 7 | "IPAddress": "192.168.126.87", 8 | "Latitude": -33.8683, 9 | "Longitude": 151.2086, 10 | "City": "Sydney", 11 | "Region": "New South Wales", 12 | "CountryCode": "AU", 13 | "CountryName": "Australia" 14 | }, 15 | { 16 | "EmailAddress": "subs+6576576576@example.com", 17 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 18 | "Date": "2010-10-08 14:24:00", 19 | "IPAddress": "192.168.126.87", 20 | "Latitude": -33.8683, 21 | "Longitude": 151.2086, 22 | "City": "Sydney", 23 | "Region": "New South Wales", 24 | "CountryCode": "AU", 25 | "CountryName": "Australia" 26 | }, 27 | { 28 | "EmailAddress": "subs+6576576576@example.com", 29 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 30 | "Date": "2010-10-07 10:20:00", 31 | "IPAddress": "192.168.126.87", 32 | "Latitude": -33.8683, 33 | "Longitude": 151.2086, 34 | "City": "Sydney", 35 | "Region": "New South Wales", 36 | "CountryCode": "AU", 37 | "CountryName": "Australia" 38 | }, 39 | { 40 | "EmailAddress": "subs+6576576576@example.com", 41 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 42 | "Date": "2010-10-07 07:15:00", 43 | "IPAddress": "192.168.126.87", 44 | "Latitude": -33.8683, 45 | "Longitude": 151.2086, 46 | "City": "Sydney", 47 | "Region": "New South Wales", 48 | "CountryCode": "AU", 49 | "CountryName": "Australia" 50 | }, 51 | { 52 | "EmailAddress": "subs+6576576576@example.com", 53 | "ListID": "512a3bc577a58fdf689c654329b50fa0", 54 | "Date": "2010-10-07 06:58:00", 55 | "IPAddress": "192.168.126.87", 56 | "Latitude": -33.8683, 57 | "Longitude": 151.2086, 58 | "City": "Sydney", 59 | "Region": "New South Wales", 60 | "CountryCode": "AU", 61 | "CountryName": "Australia" 62 | } 63 | ], 64 | "ResultsOrderedBy": "date", 65 | "OrderDirection": "asc", 66 | "PageNumber": 1, 67 | "PageSize": 1000, 68 | "RecordsOnThisPage": 5, 69 | "TotalNumberOfRecords": 5, 70 | "NumberOfPages": 1 71 | } -------------------------------------------------------------------------------- /lib/createsend/person.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Person(CreateSendBase): 8 | """Represents a person and associated functionality.""" 9 | 10 | def __init__(self, auth=None, client_id=None, email_address=None): 11 | self.client_id = client_id 12 | self.email_address = email_address 13 | super().__init__(auth) 14 | 15 | def get(self, client_id=None, email_address=None): 16 | """Gets a person by client ID and email address.""" 17 | params = {"email": email_address or self.email_address} 18 | response = self._get("/clients/%s/people.json" % 19 | (client_id or self.client_id), params=params) 20 | return json_to_py(response) 21 | 22 | def add(self, client_id, email_address, name, access_level, password): 23 | """Adds a person to a client. Password is optional and if not supplied, an invitation will be emailed to the person""" 24 | body = { 25 | "EmailAddress": email_address, 26 | "Name": name, 27 | "AccessLevel": access_level, 28 | "Password": password} 29 | response = self._post("/clients/%s/people.json" % 30 | client_id, json.dumps(body)) 31 | return json_to_py(response) 32 | 33 | def update(self, new_email_address, name, access_level, password=None): 34 | """Updates the details for a person. Password is optional and is only updated if supplied.""" 35 | params = {"email": self.email_address} 36 | body = { 37 | "EmailAddress": new_email_address, 38 | "Name": name, 39 | "AccessLevel": access_level, 40 | "Password": password} 41 | response = self._put("/clients/%s/people.json" % self.client_id, 42 | body=json.dumps(body), params=params) 43 | # Update self.email_address, so this object can continue to be used 44 | # reliably 45 | self.email_address = new_email_address 46 | 47 | def delete(self): 48 | """Deletes the person from the client.""" 49 | params = {"email": self.email_address} 50 | response = self._delete("/clients/%s/people.json" % 51 | self.client_id, params=params) 52 | -------------------------------------------------------------------------------- /test/fixtures/tx_message_details_with_statistics.json: -------------------------------------------------------------------------------- 1 | { 2 | "MessageID": "ddc697c7-0788-4df3-a71a-a7cb935f00bd", 3 | "Status": "Delivered", 4 | "SentAt": "2014-01-15T16:09:19-05:00", 5 | "SmartEmailID": "c0da9c4c-e7e4-11e4-a74d-6c4008bc7468", 6 | "CanBeResent": true, 7 | "Recipient": "Joe Smith ", 8 | "Message": { 9 | "From": "Team Webapp ", 13 | "jamesmith@example.com" 14 | ], 15 | "CC": [ 16 | "Joe Smith " 17 | ], 18 | "BCC": "joesmith@example.com", 19 | "Attachments": [ 20 | { 21 | "Name": "Invoice.pdf", 22 | "Type": "application/pdf" 23 | } 24 | ], 25 | "Body": { 26 | "Html": "...", 27 | "Text": "..." 28 | }, 29 | "Data": { 30 | "new_password_url": "https://www.mywebapp.com/newpassword?uid=jguf45hf74hbf74gf" 31 | } 32 | }, 33 | "TotalOpens": 1, 34 | "TotalClicks": 1, 35 | "Opens": [ 36 | { 37 | "EmailAddress": "jamesmith@example.com", 38 | "Date": "2009-05-18 16:45:00", 39 | "IPAddress": "192.168.0.1", 40 | "Geolocation": { 41 | "Latitude": -33.8683, 42 | "Longitude": 151.2086, 43 | "City": "Sydney", 44 | "Region": "New South Wales", 45 | "CountryCode": "AU", 46 | "CountryName": "Australia" 47 | }, 48 | "MailClient": { 49 | "Name": "Apple Mail", 50 | "OS": "OS X", 51 | "Device": "Desktop" 52 | } 53 | } 54 | ], 55 | "Clicks": [ 56 | { 57 | "EmailAddress": "jamesmith@example.com", 58 | "Date": "2009-05-18 16:45:00", 59 | "IPAddress": "192.168.0.1", 60 | "URL": "https://www.myexammple.com/index.html", 61 | "Geolocation": { 62 | "Latitude": -33.8683, 63 | "Longitude": 151.2086, 64 | "City": "Sydney", 65 | "Region": "New South Wales", 66 | "CountryCode": "AU", 67 | "CountryName": "Australia" 68 | } 69 | } 70 | ] 71 | } 72 | 73 | -------------------------------------------------------------------------------- /test/test_template.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from createsend.template import Template 4 | 5 | 6 | class TemplateTestCase: 7 | 8 | def test_create(self): 9 | client_id = '87y8d7qyw8d7yq8w7ydwqwd' 10 | t = Template() 11 | t.stub_request("templates/%s.json" % client_id, "create_template.json") 12 | template_id = t.create(client_id, "Template One", "https://templates.org/index.html", 13 | "https://templates.org/files.zip") 14 | self.assertEqual(template_id, "98y2e98y289dh89h9383891234") 15 | self.assertEqual(t.template_id, "98y2e98y289dh89h9383891234") 16 | 17 | def test_details(self): 18 | self.template.stub_request( 19 | "templates/%s.json" % self.template.template_id, "template_details.json") 20 | t = self.template.details() 21 | self.assertEqual(t.TemplateID, "98y2e98y289dh89h938389") 22 | self.assertEqual(t.Name, "Template One") 23 | self.assertEqual( 24 | t.PreviewURL, "https://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=01AF532CD8889B33&d=r&c=E816F55BFAD1A753") 25 | self.assertEqual( 26 | t.ScreenshotURL, "https://preview.createsend.com/ts/r/14/833/263/14833263.jpg?0318092600") 27 | 28 | def test_update(self): 29 | self.template.stub_request("templates/%s.json" % 30 | self.template.template_id, None) 31 | self.template.update( 32 | "Template One Updated", "https://templates.org/index.html", "https://templates.org/files.zip") 33 | 34 | def test_delete(self): 35 | self.template.stub_request("templates/%s.json" % 36 | self.template.template_id, None) 37 | self.template.delete() 38 | 39 | 40 | class OAuthTemplateTestCase(unittest.TestCase, TemplateTestCase): 41 | """Test when using OAuth to authenticate""" 42 | 43 | def setUp(self): 44 | self.template = Template( 45 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", 46 | "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, 47 | "98y2e98y289dh89h938389") 48 | 49 | 50 | class ApiKeyTemplateTestCase(unittest.TestCase, TemplateTestCase): 51 | """Test when using an API key to authenticate""" 52 | 53 | def setUp(self): 54 | self.template = Template( 55 | {'api_key': '123123123123123123123'}, 56 | "98y2e98y289dh89h938389") 57 | -------------------------------------------------------------------------------- /lib/createsend/journey_email.py: -------------------------------------------------------------------------------- 1 | from createsend.createsend import CreateSendBase 2 | from createsend.utils import json_to_py 3 | 4 | 5 | class JourneyEmail(CreateSendBase): 6 | """Represents a journey and associated functionality""" 7 | 8 | def __init__(self, auth=None, journey_email_id=None): 9 | self.journey_email_id = journey_email_id 10 | super().__init__(auth) 11 | 12 | def bounces(self, date=None, page=None, page_size=None, order_direction=None): 13 | """Retrieves the bounces for this journey email.""" 14 | return self.get_journey_email_response(date, page, page_size, order_direction, "bounces") 15 | 16 | def clicks(self, date=None, page=None, page_size=None, order_direction=None): 17 | """Retrieves the clicks for this journey email.""" 18 | return self.get_journey_email_response(date, page, page_size, order_direction, "clicks") 19 | 20 | def opens(self, date=None, page=None, page_size=None, order_direction=None): 21 | """Retrieves the opens for this journey email.""" 22 | return self.get_journey_email_response(date, page, page_size, order_direction, "opens") 23 | 24 | def recipients(self, date=None, page=None, page_size=None, order_direction=None): 25 | """Retrieves the recipients for this journey email.""" 26 | return self.get_journey_email_response(date, page, page_size, order_direction, "recipients") 27 | 28 | def unsubscribes(self, date=None, page=None, page_size=None, order_direction=None): 29 | """Retrieves the unsubscribes for this journey email.""" 30 | return self.get_journey_email_response(date, page, page_size, order_direction, "unsubscribes") 31 | 32 | def get_journey_email_response(self, date, page, page_size, order_direction, uri): 33 | """Retrieves information for the journey email - based on theuri""" 34 | params = {} 35 | if date is not None: 36 | params["date"] = date 37 | if page is not None: 38 | params["page"] = page 39 | if page_size is not None: 40 | params["pagesize"] = page_size 41 | if order_direction is not None: 42 | params["orderdirection"] = order_direction 43 | response = self._get(self.uri_for(uri), params=params) 44 | return json_to_py(response) 45 | 46 | def uri_for(self, action): 47 | return f"/journeys/email/{self.journey_email_id}/{action}.json" 48 | 49 | -------------------------------------------------------------------------------- /test/fixtures/active_subscribers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+7t8787Y@example.com", 5 | "Name": "Person One", 6 | "Date": "2010-10-25 10:28:00", 7 | "ListJoinedDate": "2010-10-24 10:28:00", 8 | "State": "Active", 9 | "CustomFields": [ 10 | { 11 | "Key": "website", 12 | "Value": "https://example.com" 13 | }, 14 | { 15 | "Key": "multi select field", 16 | "Value": "option one" 17 | }, 18 | { 19 | "Key": "multi select field", 20 | "Value": "option two" 21 | }, 22 | { 23 | "Key": "age", 24 | "Value": "24" 25 | }, 26 | { 27 | "Key": "subscription date", 28 | "Value": "2010-03-09" 29 | } 30 | ], 31 | "ReadsEmailWith": "Gmail" 32 | }, 33 | { 34 | "EmailAddress": "subs+7878787y8ggg@example.com", 35 | "Name": "Person Two", 36 | "Date": "2010-10-25 12:17:00", 37 | "ListJoinedDate": "2010-10-25 12:17:00", 38 | "State": "Active", 39 | "CustomFields": [ 40 | { 41 | "Key": "website", 42 | "Value": "https://subdomain.example.com" 43 | } 44 | ], 45 | "ReadsEmailWith": "Gmail" 46 | }, 47 | { 48 | "EmailAddress": "subs+7890909i0ggg@example.com", 49 | "Name": "Person Three", 50 | "Date": "2010-10-25 12:52:00", 51 | "ListJoinedDate": "2010-10-25 12:52:00", 52 | "State": "Active", 53 | "CustomFields": [ 54 | { 55 | "Key": "website", 56 | "Value": "https://subdomain.example.com" 57 | } 58 | ], 59 | "ReadsEmailWith": "" 60 | }, 61 | { 62 | "EmailAddress": "subs@example.com", 63 | "Name": "Person Four", 64 | "Date": "2010-10-27 13:13:00", 65 | "ListJoinedDate": "2010-10-27 13:13:00", 66 | "State": "Active", 67 | "CustomFields": [], 68 | "ReadsEmailWith": "" 69 | }, 70 | { 71 | "EmailAddress": "joey@example.com", 72 | "Name": "Person Five", 73 | "Date": "2010-10-27 13:13:00", 74 | "ListJoinedDate": "2010-10-27 13:13:00", 75 | "State": "Active", 76 | "CustomFields": [], 77 | "ReadsEmailWith": "Gmail" 78 | } 79 | ], 80 | "ResultsOrderedBy": "email", 81 | "OrderDirection": "asc", 82 | "PageNumber": 1, 83 | "PageSize": 1000, 84 | "RecordsOnThisPage": 5, 85 | "TotalNumberOfRecords": 5, 86 | "NumberOfPages": 1 87 | } -------------------------------------------------------------------------------- /test/test_administrator.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import unittest 3 | 4 | from createsend.administrator import Administrator 5 | 6 | 7 | class AdministratorTestCase: 8 | 9 | def test_get(self): 10 | email = "admin@example.com" 11 | self.administrator.stub_request( 12 | "admins.json?email=%s" % quote(email), "admin_details.json") 13 | administrator = self.administrator.get(email) 14 | self.assertEqual(administrator.EmailAddress, email) 15 | self.assertEqual(administrator.Name, "Admin One") 16 | self.assertEqual(administrator.Status, "Active") 17 | 18 | def test_get_without_args(self): 19 | email = "admin@example.com" 20 | self.administrator.stub_request( 21 | "admins.json?email=%s" % quote(email), "admin_details.json") 22 | administrator = self.administrator.get() 23 | self.assertEqual(administrator.EmailAddress, email) 24 | self.assertEqual(administrator.Name, "Admin One") 25 | self.assertEqual(administrator.Status, "Active") 26 | 27 | def test_add(self): 28 | self.administrator.stub_request("admins.json", "add_admin.json") 29 | result = self.administrator.add("admin@example.com", "Admin Name") 30 | self.assertEqual(result.EmailAddress, "admin@example.com") 31 | 32 | def test_update(self): 33 | new_email = "new_email_address@example.com" 34 | self.administrator.stub_request("admins.json?email=%s" % quote( 35 | self.administrator.email_address), None) 36 | self.administrator.update(new_email, "Admin New Name") 37 | self.assertEqual(self.administrator.email_address, new_email) 38 | 39 | def test_delete(self): 40 | self.administrator.stub_request("admins.json?email=%s" % quote( 41 | self.administrator.email_address), None) 42 | email_address = self.administrator.delete() 43 | 44 | 45 | class OAuthAdministratorTestCase(unittest.TestCase, AdministratorTestCase): 46 | """Test when using OAuth to authenticate""" 47 | 48 | def setUp(self): 49 | self.administrator = Administrator( 50 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, "admin@example.com") 51 | 52 | 53 | class ApiKeyAdministratorTestCase(unittest.TestCase, AdministratorTestCase): 54 | """Test when using an API key to authenticate""" 55 | 56 | def setUp(self): 57 | self.administrator = Administrator( 58 | {'api_key': '123123123123123123123'}, "admin@example.com") 59 | -------------------------------------------------------------------------------- /lib/createsend/segment.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Segment(CreateSendBase): 8 | """Represents a subscriber list segment and associated functionality.""" 9 | 10 | def __init__(self, auth=None, segment_id=None): 11 | self.segment_id = segment_id 12 | super().__init__(auth) 13 | 14 | def create(self, list_id, title, rulegroups): 15 | """Creates a new segment.""" 16 | body = { 17 | "Title": title, 18 | "RuleGroups": rulegroups} 19 | response = self._post("/segments/%s.json" % list_id, json.dumps(body)) 20 | self.segment_id = json_to_py(response) 21 | return self.segment_id 22 | 23 | def update(self, title, rulegroups): 24 | """Updates this segment.""" 25 | body = { 26 | "Title": title, 27 | "RuleGroups": rulegroups} 28 | response = self._put("/segments/%s.json" % 29 | self.segment_id, json.dumps(body)) 30 | 31 | def add_rulegroup(self, rulegroup): 32 | """Adds a rulegroup to this segment.""" 33 | body = rulegroup 34 | response = self._post("/segments/%s/rules.json" % 35 | self.segment_id, json.dumps(body)) 36 | 37 | def subscribers(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_information=False): 38 | """Gets the active subscribers in this segment.""" 39 | params = { 40 | "date": date, 41 | "page": page, 42 | "pagesize": page_size, 43 | "orderfield": order_field, 44 | "orderdirection": order_direction, 45 | "includetrackinginformation": include_tracking_information 46 | } 47 | response = self._get(self.uri_for("active"), params=params) 48 | return json_to_py(response) 49 | 50 | def details(self): 51 | """Gets the details of this segment""" 52 | response = self._get("/segments/%s.json" % self.segment_id) 53 | return json_to_py(response) 54 | 55 | def clear_rules(self): 56 | """Clears all rules of this segment.""" 57 | response = self._delete("/segments/%s/rules.json" % self.segment_id) 58 | 59 | def delete(self): 60 | """Deletes this segment.""" 61 | response = self._delete("/segments/%s.json" % self.segment_id) 62 | 63 | def uri_for(self, action): 64 | return f"/segments/{self.segment_id}/{action}.json" 65 | -------------------------------------------------------------------------------- /test/fixtures/active_subscribers_with_tracking_preference.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+7t8787Y@example.com", 5 | "Name": "Person One", 6 | "Date": "2010-10-25 10:28:00", 7 | "ListJoinedDate": "2010-10-24 10:28:00", 8 | "State": "Active", 9 | "CustomFields": [ 10 | { 11 | "Key": "website", 12 | "Value": "https://example.com" 13 | }, 14 | { 15 | "Key": "multi select field", 16 | "Value": "option one" 17 | }, 18 | { 19 | "Key": "multi select field", 20 | "Value": "option two" 21 | }, 22 | { 23 | "Key": "age", 24 | "Value": "24" 25 | }, 26 | { 27 | "Key": "subscription date", 28 | "Value": "2010-03-09" 29 | } 30 | ], 31 | "ReadsEmailWith": "Gmail", 32 | "ConsentToTrack": "Yes" 33 | }, 34 | { 35 | "EmailAddress": "subs+7878787y8ggg@example.com", 36 | "Name": "Person Two", 37 | "Date": "2010-10-25 12:17:00", 38 | "ListJoinedDate": "2010-10-25 12:17:00", 39 | "State": "Active", 40 | "CustomFields": [ 41 | { 42 | "Key": "website", 43 | "Value": "https://subdomain.example.com" 44 | } 45 | ], 46 | "ReadsEmailWith": "Gmail", 47 | "ConsentToTrack": "No" 48 | }, 49 | { 50 | "EmailAddress": "subs+7890909i0ggg@example.com", 51 | "Name": "Person Three", 52 | "Date": "2010-10-25 12:52:00", 53 | "ListJoinedDate": "2010-10-25 12:52:00", 54 | "State": "Active", 55 | "CustomFields": [ 56 | { 57 | "Key": "website", 58 | "Value": "https://subdomain.example.com" 59 | } 60 | ], 61 | "ReadsEmailWith": "", 62 | "ConsentToTrack": "No" 63 | }, 64 | { 65 | "EmailAddress": "subs@example.com", 66 | "Name": "Person Four", 67 | "Date": "2010-10-27 13:13:00", 68 | "ListJoinedDate": "2010-10-27 13:13:00", 69 | "State": "Active", 70 | "CustomFields": [], 71 | "ReadsEmailWith": "", 72 | "ConsentToTrack": "Yes" 73 | }, 74 | { 75 | "EmailAddress": "joey@example.com", 76 | "Name": "Person Five", 77 | "Date": "2010-10-27 13:13:00", 78 | "ListJoinedDate": "2010-10-27 13:13:00", 79 | "State": "Active", 80 | "CustomFields": [], 81 | "ReadsEmailWith": "Gmail", 82 | "ConsentToTrack": "No" 83 | } 84 | ], 85 | "ResultsOrderedBy": "email", 86 | "OrderDirection": "asc", 87 | "PageNumber": 1, 88 | "PageSize": 1000, 89 | "RecordsOnThisPage": 5, 90 | "TotalNumberOfRecords": 5, 91 | "NumberOfPages": 1 92 | } 93 | -------------------------------------------------------------------------------- /test/fixtures/campaign_recipients.json: -------------------------------------------------------------------------------- 1 | { 2 | "Results": [ 3 | { 4 | "EmailAddress": "subs+6g76t7t0@example.com", 5 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 6 | }, 7 | { 8 | "EmailAddress": "subs+6g76t7t1@example.com", 9 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 10 | }, 11 | { 12 | "EmailAddress": "subs+6g76t7t10@example.com", 13 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 14 | }, 15 | { 16 | "EmailAddress": "subs+6g76t7t100@example.com", 17 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 18 | }, 19 | { 20 | "EmailAddress": "subs+6g76t7t1000@example.com", 21 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 22 | }, 23 | { 24 | "EmailAddress": "subs+6g76t7t1001@example.com", 25 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 26 | }, 27 | { 28 | "EmailAddress": "subs+6g76t7t1002@example.com", 29 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 30 | }, 31 | { 32 | "EmailAddress": "subs+6g76t7t1003@example.com", 33 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 34 | }, 35 | { 36 | "EmailAddress": "subs+6g76t7t1004@example.com", 37 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 38 | }, 39 | { 40 | "EmailAddress": "subs+6g76t7t1005@example.com", 41 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 42 | }, 43 | { 44 | "EmailAddress": "subs+6g76t7t1006@example.com", 45 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 46 | }, 47 | { 48 | "EmailAddress": "subs+6g76t7t1007@example.com", 49 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 50 | }, 51 | { 52 | "EmailAddress": "subs+6g76t7t1008@example.com", 53 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 54 | }, 55 | { 56 | "EmailAddress": "subs+6g76t7t1009@example.com", 57 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 58 | }, 59 | { 60 | "EmailAddress": "subs+6g76t7t101@example.com", 61 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 62 | }, 63 | { 64 | "EmailAddress": "subs+6g76t7t1010@example.com", 65 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 66 | }, 67 | { 68 | "EmailAddress": "subs+6g76t7t1011@example.com", 69 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 70 | }, 71 | { 72 | "EmailAddress": "subs+6g76t7t1012@example.com", 73 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 74 | }, 75 | { 76 | "EmailAddress": "subs+6g76t7t1013@example.com", 77 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 78 | }, 79 | { 80 | "EmailAddress": "subs+6g76t7t1014@example.com", 81 | "ListID": "a994a3caf1328a16af9a69a730eaa706" 82 | } 83 | ], 84 | "ResultsOrderedBy": "email", 85 | "OrderDirection": "asc", 86 | "PageNumber": 1, 87 | "PageSize": 20, 88 | "RecordsOnThisPage": 20, 89 | "TotalNumberOfRecords": 2200, 90 | "NumberOfPages": 110 91 | } -------------------------------------------------------------------------------- /test/test_people.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import unittest 3 | 4 | from createsend.person import Person 5 | 6 | 7 | class PeopleTestCase: 8 | 9 | def test_get(self): 10 | email = "person@example.com" 11 | self.person.stub_request("clients/%s/people.json?email=%s" % 12 | (self.client_id, quote(email)), "person_details.json") 13 | person = self.person.get(self.client_id, email) 14 | self.assertEqual(person.EmailAddress, email) 15 | self.assertEqual(person.Name, "Person One") 16 | self.assertEqual(person.AccessLevel, 1023) 17 | self.assertEqual(person.Status, "Active") 18 | 19 | def test_get_without_args(self): 20 | email = "person@example.com" 21 | self.person.stub_request("clients/%s/people.json?email=%s" % 22 | (self.client_id, quote(email)), "person_details.json") 23 | person = self.person.get() 24 | self.assertEqual(person.EmailAddress, email) 25 | self.assertEqual(person.Name, "Person One") 26 | self.assertEqual(person.AccessLevel, 1023) 27 | self.assertEqual(person.Status, "Active") 28 | 29 | def test_add(self): 30 | self.person.stub_request("clients/%s/people.json" % 31 | self.client_id, "add_person.json") 32 | result = self.person.add( 33 | self.client_id, "person@example.com", "Person Name", 1023, "Password") 34 | self.assertEqual(result.EmailAddress, "person@example.com") 35 | 36 | def test_update(self): 37 | new_email = "new_email_address@example.com" 38 | self.person.stub_request("clients/%s/people.json?email=%s" % 39 | (self.client_id, quote(self.person.email_address)), None) 40 | self.person.update(new_email, "Person New Name", 31, 'blah') 41 | self.assertEqual(self.person.email_address, new_email) 42 | 43 | def test_delete(self): 44 | self.person.stub_request("clients/%s/people.json?email=%s" % 45 | (self.client_id, quote(self.person.email_address)), None) 46 | email_address = self.person.delete() 47 | 48 | 49 | class OAuthPeopleTestCase(unittest.TestCase, PeopleTestCase): 50 | """Test when using OAuth to authenticate""" 51 | 52 | def setUp(self): 53 | self.client_id = "d98h2938d9283d982u3d98u88" 54 | self.person = Person( 55 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", 56 | "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, 57 | self.client_id, "person@example.com") 58 | 59 | 60 | class ApiKeyPeopleTestCase(unittest.TestCase, PeopleTestCase): 61 | """Test when using an API key to authenticate""" 62 | 63 | def setUp(self): 64 | self.client_id = "d98h2938d9283d982u3d98u88" 65 | self.person = Person( 66 | {'api_key': '123123123123123123123'}, 67 | self.client_id, "person@example.com") 68 | -------------------------------------------------------------------------------- /test/fixtures/timezones.json: -------------------------------------------------------------------------------- 1 | [ 2 | "(GMT) Casablanca", 3 | "(GMT) Coordinated Universal Time", 4 | "(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London", 5 | "(GMT) Monrovia, Reykjavik", 6 | "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna", 7 | "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague", 8 | "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris", 9 | "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb", 10 | "(GMT+01:00) West Central Africa", 11 | "(GMT+02:00) Amman", 12 | "(GMT+02:00) Athens, Bucharest, Istanbul", 13 | "(GMT+02:00) Beirut", 14 | "(GMT+02:00) Cairo", 15 | "(GMT+02:00) Damascus", 16 | "(GMT+02:00) Harare, Pretoria", 17 | "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius", 18 | "(GMT+02:00) Jerusalem", 19 | "(GMT+02:00) Minsk", 20 | "(GMT+02:00) Windhoek", 21 | "(GMT+03:00) Baghdad", 22 | "(GMT+03:00) Kuwait, Riyadh", 23 | "(GMT+03:00) Moscow, St. Petersburg, Volgograd", 24 | "(GMT+03:00) Nairobi", 25 | "(GMT+03:30) Tehran", 26 | "(GMT+04:00) Abu Dhabi, Muscat", 27 | "(GMT+04:00) Baku", 28 | "(GMT+04:00) Port Louis", 29 | "(GMT+04:00) Tbilisi", 30 | "(GMT+04:00) Yerevan", 31 | "(GMT+04:30) Kabul", 32 | "(GMT+05:00) Ekaterinburg", 33 | "(GMT+05:00) Islamabad, Karachi", 34 | "(GMT+05:00) Tashkent", 35 | "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi", 36 | "(GMT+05:30) Sri Jayawardenepura", 37 | "(GMT+05:45) Kathmandu", 38 | "(GMT+06:00) Astana", 39 | "(GMT+06:00) Dhaka", 40 | "(GMT+06:00) Novosibirsk", 41 | "(GMT+06:30) Yangon (Rangoon)", 42 | "(GMT+07:00) Bangkok, Hanoi, Jakarta", 43 | "(GMT+07:00) Krasnoyarsk", 44 | "(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi", 45 | "(GMT+08:00) Irkutsk", 46 | "(GMT+08:00) Kuala Lumpur, Singapore", 47 | "(GMT+08:00) Perth", 48 | "(GMT+08:00) Taipei", 49 | "(GMT+08:00) Ulaanbaatar", 50 | "(GMT+09:00) Osaka, Sapporo, Tokyo", 51 | "(GMT+09:00) Seoul", 52 | "(GMT+09:00) Yakutsk", 53 | "(GMT+09:30) Adelaide", 54 | "(GMT+09:30) Darwin", 55 | "(GMT+10:00) Brisbane", 56 | "(GMT+10:00) Canberra, Melbourne, Sydney", 57 | "(GMT+10:00) Guam, Port Moresby", 58 | "(GMT+10:00) Hobart", 59 | "(GMT+10:00) Vladivostok", 60 | "(GMT+11:00) Magadan, Solomon Is., New Caledonia", 61 | "(GMT+12:00) Auckland, Wellington", 62 | "(GMT+12:00) Coordinated Universal Time+12", 63 | "(GMT+12:00) Fiji", 64 | "(GMT+12:00) Petropavlovsk-Kamchatsky - Old", 65 | "(GMT+13:00) Nuku'alofa", 66 | "(GMT-01:00) Azores", 67 | "(GMT-01:00) Cape Verde Is.", 68 | "(GMT-02:00) Coordinated Universal Time-02", 69 | "(GMT-02:00) Mid-Atlantic", 70 | "(GMT-03:00) Brasilia", 71 | "(GMT-03:00) Buenos Aires", 72 | "(GMT-03:00) Cayenne, Fortaleza", 73 | "(GMT-03:00) Greenland", 74 | "(GMT-03:00) Montevideo", 75 | "(GMT-03:30) Newfoundland", 76 | "(GMT-04:00) Asuncion", 77 | "(GMT-04:00) Atlantic Time (Canada)", 78 | "(GMT-04:00) Cuiaba", 79 | "(GMT-04:00) Georgetown, La Paz, Manaus, San Juan", 80 | "(GMT-04:00) Santiago", 81 | "(GMT-04:30) Caracas", 82 | "(GMT-05:00) Bogota, Lima, Quito", 83 | "(GMT-05:00) Eastern Time (US & Canada)", 84 | "(GMT-05:00) Indiana (East)", 85 | "(GMT-06:00) Central America", 86 | "(GMT-06:00) Central Time (US & Canada)", 87 | "(GMT-06:00) Guadalajara, Mexico City, Monterrey", 88 | "(GMT-06:00) Saskatchewan", 89 | "(GMT-07:00) Arizona", 90 | "(GMT-07:00) Chihuahua, La Paz, Mazatlan", 91 | "(GMT-07:00) Mountain Time (US & Canada)", 92 | "(GMT-08:00) Baja California", 93 | "(GMT-08:00) Pacific Time (US & Canada)", 94 | "(GMT-09:00) Alaska", 95 | "(GMT-10:00) Hawaii", 96 | "(GMT-11:00) Coordinated Universal Time-11", 97 | "(GMT-11:00) Samoa", 98 | "(GMT-12:00) International Date Line West" 99 | ] -------------------------------------------------------------------------------- /lib/createsend/transactional.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py, validate_consent_to_track 5 | 6 | 7 | class Transactional(CreateSendBase): 8 | """Represents transactional functionality.""" 9 | 10 | def __init__(self, auth=None, client_id=None): 11 | self.client_id = client_id 12 | super().__init__(auth) 13 | 14 | def smart_email_list(self, status="all", client_id=None): 15 | """Gets the smart email list.""" 16 | if client_id is None: 17 | response = self._get( 18 | "/transactional/smartEmail?status=%s" % status) 19 | else: 20 | response = self._get( 21 | f"/transactional/smartEmail?status={status}&clientID={client_id}") 22 | return json_to_py(response) 23 | 24 | def smart_email_details(self, smart_email_id): 25 | """Gets the smart email details.""" 26 | response = self._get("/transactional/smartEmail/%s" % smart_email_id) 27 | return json_to_py(response) 28 | 29 | def smart_email_send(self, smart_email_id, to, consent_to_track, cc=None, bcc=None, attachments=None, data=None, add_recipients_to_list=None): 30 | """Sends the smart email.""" 31 | validate_consent_to_track(consent_to_track) 32 | body = { 33 | "To": to, 34 | "CC": cc, 35 | "BCC": bcc, 36 | "Attachments": attachments, 37 | "Data": data, 38 | "AddRecipientsToList": add_recipients_to_list, 39 | "ConsentToTrack": consent_to_track, 40 | } 41 | response = self._post("/transactional/smartEmail/%s/send" % 42 | smart_email_id, json.dumps(body)) 43 | return json_to_py(response) 44 | 45 | def classic_email_send(self, subject, from_address, to, consent_to_track, client_id=None, cc=None, bcc=None, html=None, text=None, attachments=None, track_opens=True, track_clicks=True, inline_css=True, group=None, add_recipients_to_list=None): 46 | """Sends a classic email.""" 47 | validate_consent_to_track(consent_to_track) 48 | body = { 49 | "Subject": subject, 50 | "From": from_address, 51 | "To": to, 52 | "CC": cc, 53 | "BCC": bcc, 54 | "HTML": html, 55 | "Text": text, 56 | "Attachments": attachments, 57 | "TrackOpens": track_opens, 58 | "TrackClicks": track_clicks, 59 | "InlineCSS": inline_css, 60 | "Group": group, 61 | "AddRecipientsToList": add_recipients_to_list, 62 | "ConsentToTrack": consent_to_track, 63 | } 64 | if client_id is None: 65 | response = self._post( 66 | "/transactional/classicEmail/send", json.dumps(body)) 67 | else: 68 | response = self._post( 69 | "/transactional/classicEmail/send?clientID=%s" % client_id, json.dumps(body)) 70 | return json_to_py(response) 71 | 72 | def classic_email_groups(self, client_id=None): 73 | """Gets the list of classic email groups.""" 74 | if client_id is None: 75 | response = self._get("/transactional/classicEmail/groups") 76 | else: 77 | response = self._get( 78 | "/transactional/classicEmail/groups?clientID=%s" % client_id) 79 | return json_to_py(response) 80 | 81 | def statistics(self, params={}): 82 | """Gets the statistics according to the parameters.""" 83 | response = self._get("/transactional/statistics", params) 84 | return json_to_py(response) 85 | 86 | def message_timeline(self, params={}): 87 | """Gets the messages timeline according to the parameters.""" 88 | response = self._get("/transactional/messages", params) 89 | return json_to_py(response) 90 | 91 | def message_details(self, message_id, statistics=False, exclude_message_body=False): 92 | """Gets the details of this message.""" 93 | response = self._get( 94 | f"/transactional/messages/{message_id}?statistics={statistics}&excludemessagebody={exclude_message_body}") 95 | return json_to_py(response) 96 | 97 | def message_resend(self, message_id): 98 | """Resend the message.""" 99 | response = self._post("/transactional/messages/%s/resend" % message_id) 100 | return json_to_py(response) 101 | -------------------------------------------------------------------------------- /test/fixtures/countries.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Afghanistan", 3 | "Albania", 4 | "Algeria", 5 | "American Samoa", 6 | "Andorra", 7 | "Angola", 8 | "Anguilla", 9 | "Antigua & Barbuda", 10 | "Argentina", 11 | "Armenia", 12 | "Aruba", 13 | "Australia", 14 | "Austria", 15 | "Azerbaijan", 16 | "Azores", 17 | "Bahamas", 18 | "Bahrain", 19 | "Bangladesh", 20 | "Barbados", 21 | "Belarus", 22 | "Belgium", 23 | "Belize", 24 | "Benin", 25 | "Bermuda", 26 | "Bhutan", 27 | "Bolivia", 28 | "Bonaire", 29 | "Bosnia & Herzegovina", 30 | "Botswana", 31 | "Brazil", 32 | "British Indian Ocean Ter", 33 | "Brunei", 34 | "Bulgaria", 35 | "Burkina Faso", 36 | "Burundi", 37 | "Cambodia", 38 | "Cameroon", 39 | "Canada", 40 | "Canary Islands", 41 | "Cape Verde", 42 | "Cayman Islands", 43 | "Central African Republic", 44 | "Chad", 45 | "Channel Islands", 46 | "Chile", 47 | "China", 48 | "Christmas Island", 49 | "Cocos Island", 50 | "Columbia", 51 | "Comoros", 52 | "Congo", 53 | "Congo Democratic Rep", 54 | "Cook Islands", 55 | "Costa Rica", 56 | "Cote D'Ivoire", 57 | "Croatia", 58 | "Cuba", 59 | "Curacao", 60 | "Cyprus", 61 | "Czech Republic", 62 | "Denmark", 63 | "Djibouti", 64 | "Dominica", 65 | "Dominican Republic", 66 | "East Timor", 67 | "Ecuador", 68 | "Egypt", 69 | "El Salvador", 70 | "Equatorial Guinea", 71 | "Eritrea", 72 | "Estonia", 73 | "Ethiopia", 74 | "Falkland Islands", 75 | "Faroe Islands", 76 | "Fiji", 77 | "Finland", 78 | "France", 79 | "French Guiana", 80 | "French Polynesia", 81 | "French Southern Ter", 82 | "Gabon", 83 | "Gambia", 84 | "Georgia", 85 | "Germany", 86 | "Ghana", 87 | "Gibraltar", 88 | "Great Britain", 89 | "Greece", 90 | "Greenland", 91 | "Grenada", 92 | "Guadeloupe", 93 | "Guam", 94 | "Guatemala", 95 | "Guinea", 96 | "Guyana", 97 | "Haiti", 98 | "Honduras", 99 | "Hong Kong", 100 | "Hungary", 101 | "Iceland", 102 | "India", 103 | "Indonesia", 104 | "Iran", 105 | "Iraq", 106 | "Ireland", 107 | "Isle of Man", 108 | "Israel", 109 | "Italy", 110 | "Jamaica", 111 | "Japan", 112 | "Jordan", 113 | "Kazakhstan", 114 | "Kenya", 115 | "Kiribati", 116 | "Korea North", 117 | "Korea South", 118 | "Kuwait", 119 | "Kyrgyzstan", 120 | "Laos", 121 | "Latvia", 122 | "Lebanon", 123 | "Lesotho", 124 | "Liberia", 125 | "Libya", 126 | "Liechtenstein", 127 | "Lithuania", 128 | "Luxembourg", 129 | "Macau", 130 | "Macedonia", 131 | "Madagascar", 132 | "Malawi", 133 | "Malaysia", 134 | "Maldives", 135 | "Mali", 136 | "Malta", 137 | "Marshall Islands", 138 | "Martinique", 139 | "Mauritania", 140 | "Mauritius", 141 | "Mayotte", 142 | "Mexico", 143 | "Midway Islands", 144 | "Moldova", 145 | "Monaco", 146 | "Mongolia", 147 | "Montserrat", 148 | "Morocco", 149 | "Mozambique", 150 | "Myanmar", 151 | "Namibia", 152 | "Nauru", 153 | "Nepal", 154 | "Netherland Antilles", 155 | "Netherlands", 156 | "Nevis", 157 | "New Caledonia", 158 | "New Zealand", 159 | "Nicaragua", 160 | "Niger", 161 | "Nigeria", 162 | "Niue", 163 | "Norfolk Island", 164 | "Norway", 165 | "Oman", 166 | "Pakistan", 167 | "Palau Island", 168 | "Palestine", 169 | "Panama", 170 | "Papua New Guinea", 171 | "Paraguay", 172 | "Peru", 173 | "Philippines", 174 | "Pitcairn Island", 175 | "Poland", 176 | "Portugal", 177 | "Puerto Rico", 178 | "Qatar", 179 | "Reunion", 180 | "Romania", 181 | "Russia", 182 | "Rwanda", 183 | "Saipan", 184 | "Samoa", 185 | "Samoa American", 186 | "San Marino", 187 | "Sao Tome & Principe", 188 | "Saudi Arabia", 189 | "Senegal", 190 | "Serbia & Montenegro", 191 | "Seychelles", 192 | "Sierra Leone", 193 | "Singapore", 194 | "Slovakia", 195 | "Slovenia", 196 | "Solomon Islands", 197 | "Somalia", 198 | "South Africa", 199 | "Spain", 200 | "Sri Lanka", 201 | "St Barthelemy", 202 | "St Eustatius", 203 | "St Helena", 204 | "St Kitts-Nevis", 205 | "St Lucia", 206 | "St Maarten", 207 | "St Pierre & Miquelon", 208 | "St Vincent & Grenadines", 209 | "Sudan", 210 | "Suriname", 211 | "Swaziland", 212 | "Sweden", 213 | "Switzerland", 214 | "Syria", 215 | "Tahiti", 216 | "Taiwan", 217 | "Tajikistan", 218 | "Tanzania", 219 | "Thailand", 220 | "Togo", 221 | "Tokelau", 222 | "Tonga", 223 | "Trinidad & Tobago", 224 | "Tunisia", 225 | "Turkey", 226 | "Turkmenistan", 227 | "Turks & Caicos Is", 228 | "Tuvalu", 229 | "Uganda", 230 | "Ukraine", 231 | "United Arab Emirates", 232 | "United Kingdom", 233 | "United States of America", 234 | "Uruguay", 235 | "Uzbekistan", 236 | "Vanuatu", 237 | "Vatican City State", 238 | "Venezuela", 239 | "Vietnam", 240 | "Virgin Islands (Brit)", 241 | "Virgin Islands (USA)", 242 | "Wake Island", 243 | "Wallis & Futana Is", 244 | "Yemen", 245 | "Zambia", 246 | "Zimbabwe" 247 | ] -------------------------------------------------------------------------------- /lib/createsend/subscriber.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase, BadRequest 4 | from createsend.utils import json_to_py, validate_consent_to_track 5 | 6 | 7 | class Subscriber(CreateSendBase): 8 | """Represents a subscriber and associated functionality.""" 9 | 10 | def __init__(self, auth=None, list_id=None, email_address=None): 11 | self.list_id = list_id 12 | self.email_address = email_address 13 | super().__init__(auth) 14 | 15 | def get(self, list_id=None, email_address=None, include_tracking_preference=False): 16 | """Gets a subscriber by list ID and email address.""" 17 | params = { 18 | "email": email_address or self.email_address, 19 | "includetrackingpreference": include_tracking_preference, 20 | } 21 | response = self._get("/subscribers/%s.json" % 22 | (list_id or self.list_id), params=params) 23 | return json_to_py(response) 24 | 25 | def add(self, list_id, email_address, name, custom_fields, resubscribe, consent_to_track, restart_subscription_based_autoresponders=False, mobile_number=False, consent_to_track_sms="Unchanged"): 26 | """Adds a subscriber to a subscriber list.""" 27 | validate_consent_to_track(consent_to_track) 28 | body = { 29 | "EmailAddress": email_address, 30 | "Name": name, 31 | "CustomFields": custom_fields, 32 | "Resubscribe": resubscribe, 33 | "ConsentToTrack": consent_to_track, 34 | "RestartSubscriptionBasedAutoresponders": restart_subscription_based_autoresponders} 35 | 36 | if mobile_number: 37 | body["MobileNumber"] = mobile_number 38 | validate_consent_to_track(consent_to_track_sms) 39 | body["ConsentToSendSms"] = consent_to_track_sms 40 | 41 | response = self._post("/subscribers/%s.json" % 42 | list_id, json.dumps(body)) 43 | return json_to_py(response) 44 | 45 | def update(self, new_email_address, name, custom_fields, resubscribe, consent_to_track, restart_subscription_based_autoresponders=False, mobile_number=False, consent_to_track_sms="Unchanged"): 46 | """Updates any aspect of a subscriber, including email address, name, and 47 | custom field data if supplied.""" 48 | validate_consent_to_track(consent_to_track) 49 | params = {"email": self.email_address} 50 | body = { 51 | "EmailAddress": new_email_address, 52 | "Name": name, 53 | "CustomFields": custom_fields, 54 | "Resubscribe": resubscribe, 55 | "ConsentToTrack": consent_to_track, 56 | "RestartSubscriptionBasedAutoresponders": restart_subscription_based_autoresponders} 57 | 58 | if mobile_number: 59 | body["MobileNumber"] = mobile_number 60 | validate_consent_to_track(consent_to_track_sms) 61 | body["ConsentToSendSms"] = consent_to_track_sms 62 | 63 | response = self._put("/subscribers/%s.json" % self.list_id, 64 | body=json.dumps(body), params=params) 65 | # Update self.email_address, so this object can continue to be used 66 | # reliably 67 | self.email_address = new_email_address 68 | 69 | def import_subscribers(self, list_id, subscribers, resubscribe, queue_subscription_based_autoresponders=False, restart_subscription_based_autoresponders=False): 70 | """Imports subscribers into a subscriber list.""" 71 | body = { 72 | "Subscribers": subscribers, 73 | "Resubscribe": resubscribe, 74 | "QueueSubscriptionBasedAutoresponders": queue_subscription_based_autoresponders, 75 | "RestartSubscriptionBasedAutoresponders": restart_subscription_based_autoresponders} 76 | try: 77 | response = self._post("/subscribers/%s/import.json" % 78 | list_id, json.dumps(body)) 79 | except BadRequest as br: 80 | # Subscriber import will throw BadRequest if some subscribers are not imported 81 | # successfully. If this occurs, we want to return the ResultData property of 82 | # the BadRequest exception (which is of the same "form" as the response we'd 83 | # receive upon a completely successful import) 84 | if hasattr(br.data, 'ResultData'): 85 | return br.data.ResultData 86 | else: 87 | raise br 88 | return json_to_py(response) 89 | 90 | def unsubscribe(self): 91 | """Unsubscribes this subscriber from the associated list.""" 92 | body = { 93 | "EmailAddress": self.email_address} 94 | response = self._post("/subscribers/%s/unsubscribe.json" % 95 | self.list_id, json.dumps(body)) 96 | 97 | def history(self): 98 | """Gets the historical record of this subscriber's trackable actions.""" 99 | params = {"email": self.email_address} 100 | response = self._get("/subscribers/%s/history.json" % 101 | self.list_id, params=params) 102 | return json_to_py(response) 103 | 104 | def delete(self): 105 | """Moves this subscriber to the deleted state in the associated list.""" 106 | params = {"email": self.email_address} 107 | response = self._delete("/subscribers/%s.json" % 108 | self.list_id, params=params) -------------------------------------------------------------------------------- /lib/createsend/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from http.client import HTTPSConnection 4 | import socket 5 | import ssl 6 | import json 7 | 8 | 9 | 10 | VALID_CONSENT_TO_TRACK_VALUES = ("yes", "no", "unchanged") 11 | 12 | 13 | class CertificateError(ValueError): 14 | """ 15 | Raised when an error occurs when attempting to verify an SSL certificate. 16 | """ 17 | pass 18 | 19 | 20 | def _dnsname_to_pat(dn): 21 | pats = [] 22 | for frag in dn.split(r'.'): 23 | if frag == '*': 24 | # When '*' is a fragment by itself, it matches a non-empty dotless 25 | # fragment. 26 | pats.append('[^.]+') 27 | else: 28 | # Otherwise, '*' matches any dotless fragment. 29 | frag = re.escape(frag) 30 | pats.append(frag.replace(r'\*', '[^.]*')) 31 | return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) 32 | 33 | 34 | def match_hostname(cert, hostname): 35 | """ 36 | This is a backport of the match_hostname() function from Python 3.2, 37 | essential when using SSL. 38 | Verifies that *cert* (in decoded format as returned by 39 | SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules 40 | are mostly followed, but IP addresses are not accepted for *hostname*. 41 | 42 | CertificateError is raised on failure. On success, the function 43 | returns nothing. 44 | """ 45 | if not cert: 46 | raise ValueError("empty or no certificate") 47 | dnsnames = [] 48 | san = cert.get('subjectAltName', ()) 49 | for key, value in san: 50 | if key == 'DNS': 51 | if _dnsname_to_pat(value).match(hostname): 52 | return 53 | dnsnames.append(value) 54 | if not san: 55 | # The subject is only checked when subjectAltName is empty 56 | for sub in cert.get('subject', ()): 57 | for key, value in sub: 58 | # XXX according to RFC 2818, the most specific Common Name 59 | # must be used. 60 | if key == 'commonName': 61 | if _dnsname_to_pat(value).match(hostname): 62 | return 63 | dnsnames.append(value) 64 | if len(dnsnames) > 1: 65 | raise CertificateError("hostname %r " 66 | "doesn't match either of %s" 67 | % (hostname, ', '.join(map(repr, dnsnames)))) 68 | elif len(dnsnames) == 1: 69 | raise CertificateError("hostname %r " 70 | "doesn't match %r" 71 | % (hostname, dnsnames[0])) 72 | else: 73 | raise CertificateError("no appropriate commonName or " 74 | "subjectAltName fields were found") 75 | 76 | 77 | class VerifiedHTTPSConnection(HTTPSConnection): 78 | """ 79 | A connection that includes SSL certificate verification. 80 | """ 81 | 82 | def connect(self): 83 | self.connection_kwargs = {} 84 | self.connection_kwargs.update(timeout=self.timeout) 85 | self.connection_kwargs.update(source_address=self.source_address) 86 | 87 | sock = socket.create_connection( 88 | (self.host, self.port), **self.connection_kwargs) 89 | 90 | if self._tunnel_host: 91 | self._tunnel() 92 | 93 | cert_path = os.path.join(os.path.dirname(__file__), 'cacert.pem') 94 | 95 | context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) 96 | context.verify_mode = ssl.CERT_REQUIRED 97 | context.load_verify_locations(cert_path) 98 | if hasattr(self, 'cert_file') and hasattr(self, 'key_file') and self.cert_file and self.key_file: 99 | context.load_cert_chain(certfile=self.cert_file, keyfile=self.key_file) 100 | self.sock = context.wrap_socket(sock, server_hostname=self.host) 101 | 102 | try: 103 | match_hostname(self.sock.getpeercert(), self.host) 104 | except CertificateError: 105 | self.sock.shutdown(socket.SHUT_RDWR) 106 | self.sock.close() 107 | raise 108 | 109 | 110 | def json_to_py(o): 111 | if isinstance(o,bytes): 112 | o = json.loads(o.decode('utf-8')) 113 | if isinstance(o, dict): 114 | return dict_to_object(o) 115 | else: 116 | return dict_to_object({"response": o}).response 117 | 118 | 119 | def dict_to_object(d): 120 | """Recursively converts a dict to an object""" 121 | top = type('CreateSendModel', (object,), d) 122 | seqs = tuple, list, set, frozenset 123 | for i, j in list(d.items()): 124 | if isinstance(j, dict): 125 | setattr(top, i, dict_to_object(j)) 126 | elif isinstance(j, seqs): 127 | setattr(top, i, type(j)(dict_to_object(sj) 128 | if isinstance(sj, dict) else sj for sj in j)) 129 | else: 130 | setattr(top, i, j) 131 | return top 132 | 133 | 134 | def validate_consent_to_track(user_input): 135 | from createsend import ClientError 136 | if hasattr(user_input, 'lower'): 137 | user_input = user_input.lower() 138 | if user_input in VALID_CONSENT_TO_TRACK_VALUES: 139 | return 140 | raise ClientError(f"Consent to track value must be one of {VALID_CONSENT_TO_TRACK_VALUES}") 141 | 142 | 143 | def get_faker(expected_url, filename, status=None, body=None): 144 | 145 | class Faker: 146 | """Represents a fake web request, including the expected URL, an open 147 | function which reads the expected response from a fixture file, and the 148 | expected response status code.""" 149 | 150 | def __init__(self, expected_url, filename, status, body=None): 151 | self.url = self.createsend_url(expected_url) 152 | self.filename = filename 153 | self.status = status 154 | self.body = body 155 | 156 | def open(self): 157 | if self.filename: 158 | return open(f"{os.path.dirname(os.path.dirname(__file__))}/../test/fixtures/{self.filename}", mode='rb').read() 159 | else: 160 | return '' 161 | 162 | def createsend_url(self, url): 163 | if url.startswith("http"): 164 | return url 165 | else: 166 | return "https://api.createsend.com/api/v3.3/%s" % url 167 | 168 | return Faker(expected_url, filename, status, body) 169 | -------------------------------------------------------------------------------- /test/test_segment.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import unittest 3 | 4 | from createsend.segment import Segment 5 | 6 | 7 | class SegmentTestCase: 8 | 9 | def test_create(self): 10 | list_id = "2983492834987394879837498" 11 | rulegroups = [{"Rules": [{"RuleType": "EmailAddress", "Clause": "CONTAINS example.com"}, { 12 | "RuleType": "Name", "Clause": "EQUALS subscriber"}]}] 13 | s = Segment() 14 | s.stub_request("segments/%s.json" % list_id, "create_segment.json", None, 15 | "{\"RuleGroups\": [{\"Rules\": [{\"Clause\": \"CONTAINS example.com\", \"RuleType\": \"EmailAddress\"}, {\"Clause\": \"EQUALS subscriber\", \"RuleType\": \"Name\"}]}], \"Title\": \"new segment title\"}") 16 | segment_id = s.create(list_id, "new segment title", rulegroups) 17 | self.assertEqual(segment_id, "0246c2aea610a3545d9780bf6ab890061234") 18 | self.assertEqual(s.segment_id, "0246c2aea610a3545d9780bf6ab890061234") 19 | 20 | def test_update(self): 21 | rulegroups = [ 22 | {"Rules": [{"RuleType": "Name", "Clause": "EQUALS subscriber"}]}] 23 | self.segment.stub_request("segments/%s.json" % self.segment.segment_id, None, 24 | "{\"Rules\": [{\"Clause\": \"EQUALS subscriber\", \"RuleType\": \"Name\"}], \"Title\": \"new title for segment\"}") 25 | self.segment.update("new title for segment", rulegroups) 26 | 27 | def test_add_rulegroup(self): 28 | rulegroup = { 29 | "Rules": [{"RuleType": "EmailAddress", "Clause": "CONTAINS example.com"}]} 30 | self.segment.stub_request("segments/%s/rules.json" % self.segment.segment_id, None, None, 31 | "{\"Rules\": [{\"Clause\": \"CONTAINS example.com\", \"RuleType\": \"EmailAddress\"}]}") 32 | self.segment.add_rulegroup(rulegroup) 33 | 34 | def test_subscribers(self): 35 | min_date = "2010-01-01" 36 | self.segment.stub_request("segments/%s/active.json?date=%s&orderfield=email&page=1&pagesize=1000&orderdirection=asc&includetrackinginformation=False" % 37 | (self.segment.segment_id, quote(min_date)), "segment_subscribers.json") 38 | res = self.segment.subscribers(min_date) 39 | self.assertEqual(res.ResultsOrderedBy, "email") 40 | self.assertEqual(res.OrderDirection, "asc") 41 | self.assertEqual(res.PageNumber, 1) 42 | self.assertEqual(res.PageSize, 1000) 43 | self.assertEqual(res.RecordsOnThisPage, 2) 44 | self.assertEqual(res.TotalNumberOfRecords, 2) 45 | self.assertEqual(res.NumberOfPages, 1) 46 | self.assertEqual(len(res.Results), 2) 47 | self.assertEqual(res.Results[0].EmailAddress, "personone@example.com") 48 | self.assertEqual(res.Results[0].Name, "Person One") 49 | self.assertEqual(res.Results[0].Date, "2010-10-27 13:13:00") 50 | self.assertEqual(res.Results[0].ListJoinedDate, "2010-10-27 13:13:00") 51 | self.assertEqual(res.Results[0].State, "Active") 52 | self.assertEqual(res.Results[0].CustomFields, []) 53 | 54 | def test_subscribers_with_tracking_information_included(self): 55 | min_date = "2010-01-01" 56 | self.segment.stub_request("segments/%s/active.json?date=%s&orderfield=email&page=1&pagesize=1000&orderdirection=asc&includetrackinginformation=True" % 57 | (self.segment.segment_id, quote(min_date)), "segment_subscribers_with_tracking_preference.json") 58 | res = self.segment.subscribers(min_date, include_tracking_information=True) 59 | self.assertEqual(res.ResultsOrderedBy, "email") 60 | self.assertEqual(res.OrderDirection, "asc") 61 | self.assertEqual(res.PageNumber, 1) 62 | self.assertEqual(res.PageSize, 1000) 63 | self.assertEqual(res.RecordsOnThisPage, 2) 64 | self.assertEqual(res.TotalNumberOfRecords, 2) 65 | self.assertEqual(res.NumberOfPages, 1) 66 | self.assertEqual(len(res.Results), 2) 67 | self.assertEqual(res.Results[0].EmailAddress, "personone@example.com") 68 | self.assertEqual(res.Results[0].Name, "Person One") 69 | self.assertEqual(res.Results[0].Date, "2010-10-27 13:13:00") 70 | self.assertEqual(res.Results[0].ListJoinedDate, "2010-10-27 13:13:00") 71 | self.assertEqual(res.Results[0].State, "Active") 72 | self.assertEqual(res.Results[0].CustomFields, []) 73 | self.assertEqual(res.Results[0].ConsentToTrack, "Yes") 74 | 75 | def test_delete(self): 76 | self.segment.stub_request("segments/%s.json" % 77 | self.segment.segment_id, None) 78 | self.segment.delete() 79 | 80 | def test_clear_rules(self): 81 | self.segment.stub_request("segments/%s/rules.json" % 82 | self.segment.segment_id, None) 83 | self.segment.clear_rules() 84 | 85 | def test_details(self): 86 | self.segment.stub_request("segments/%s.json" % 87 | self.segment.segment_id, "segment_details.json") 88 | res = self.segment.details() 89 | self.assertEqual(res.ActiveSubscribers, 0) 90 | self.assertEqual(len(res.RuleGroups), 2) 91 | self.assertEqual(res.RuleGroups[0].Rules[0].RuleType, "EmailAddress") 92 | self.assertEqual(res.RuleGroups[0].Rules[ 93 | 0].Clause, "CONTAINS @hello.com") 94 | self.assertEqual(res.RuleGroups[1].Rules[0].RuleType, "Name") 95 | self.assertEqual(res.RuleGroups[1].Rules[0].Clause, "PROVIDED") 96 | self.assertEqual(res.ListID, "2bea949d0bf96148c3e6a209d2e82060") 97 | self.assertEqual(res.SegmentID, "dba84a225d5ce3d19105d7257baac46f") 98 | self.assertEqual(res.Title, "My Segment") 99 | 100 | 101 | class OAuthSegmentTestCase(unittest.TestCase, SegmentTestCase): 102 | """Test when using OAuth to authenticate""" 103 | 104 | def setUp(self): 105 | self.segment_id = "98y2e98y289dh89h938389" 106 | self.segment = Segment( 107 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, self.segment_id) 108 | 109 | 110 | class ApiKeySegmentTestCase(unittest.TestCase, SegmentTestCase): 111 | """Test when using an API key to authenticate""" 112 | 113 | def setUp(self): 114 | self.segment_id = "98y2e98y289dh89h938389" 115 | self.segment = Segment( 116 | {'api_key': '123123123123123123123'}, self.segment_id) 117 | -------------------------------------------------------------------------------- /test/test_authentication.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import unittest 3 | 4 | from createsend.createsend import CreateSend, ExpiredOAuthToken 5 | 6 | 7 | class AuthenticationTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.oauth_auth_details = { 11 | "access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="} 12 | self.api_key_auth_details = {'api_key': '123123123123123123123'} 13 | 14 | def test_authorize_url_with_state(self): 15 | client_id = 8998879 16 | redirect_uri = 'https://example.com/auth' 17 | scope = 'ViewReports,CreateCampaigns,SendCampaigns' 18 | state = 89879287 19 | 20 | self.cs = CreateSend(self.oauth_auth_details) 21 | authorize_url = self.cs.authorize_url( 22 | client_id=client_id, 23 | redirect_uri=redirect_uri, 24 | scope=scope, 25 | state=state 26 | ) 27 | self.assertEqual(authorize_url, 28 | "https://api.createsend.com/oauth?client_id=8998879&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&scope=ViewReports%2CCreateCampaigns%2CSendCampaigns&state=89879287" 29 | ) 30 | 31 | def test_authorize_url_without_state(self): 32 | client_id = 8998879 33 | redirect_uri = 'https://example.com/auth' 34 | scope = 'ViewReports,CreateCampaigns,SendCampaigns' 35 | 36 | self.cs = CreateSend(self.oauth_auth_details) 37 | authorize_url = self.cs.authorize_url( 38 | client_id=client_id, 39 | redirect_uri=redirect_uri, 40 | scope=scope 41 | ) 42 | self.assertEqual(authorize_url, 43 | "https://api.createsend.com/oauth?client_id=8998879&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&scope=ViewReports%2CCreateCampaigns%2CSendCampaigns" 44 | ) 45 | 46 | def test_exchange_token_success(self): 47 | client_id = 8998879 48 | client_secret = 'iou0q9wud0q9wd0q9wid0q9iwd0q9wid0q9wdqwd' 49 | redirect_uri = 'https://example.com/auth' 50 | code = '98uqw9d8qu9wdu' 51 | self.cs = CreateSend(self.oauth_auth_details) 52 | self.cs.stub_request( 53 | "https://api.createsend.com/oauth/token", "oauth_exchange_token.json") 54 | access_token, expires_in, refresh_token = self.cs.exchange_token( 55 | client_id=client_id, 56 | client_secret=client_secret, 57 | redirect_uri=redirect_uri, 58 | code=code 59 | ) 60 | self.assertEqual(self.cs.faker.actual_body, 61 | "grant_type=authorization_code&client_id=8998879&client_secret=iou0q9wud0q9wd0q9wid0q9iwd0q9wid0q9wdqwd&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&code=98uqw9d8qu9wdu") 62 | self.assertEqual(access_token, "SlAV32hkKG") 63 | self.assertEqual(expires_in, 1209600) 64 | self.assertEqual(refresh_token, "tGzv3JOkF0XG5Qx2TlKWIA") 65 | 66 | def test_echange_token_failure(self): 67 | client_id = 8998879 68 | client_secret = 'iou0q9wud0q9wd0q9wid0q9iwd0q9wid0q9wdqwd' 69 | redirect_uri = 'https://example.com/auth' 70 | code = 'invalidcode' 71 | self.cs = CreateSend(self.oauth_auth_details) 72 | self.cs.stub_request( 73 | "https://api.createsend.com/oauth/token", "oauth_exchange_token_error.json") 74 | self.assertRaises(Exception, self.cs.exchange_token, 75 | client_id, client_secret, redirect_uri, code) 76 | self.assertEqual(self.cs.faker.actual_body, 77 | "grant_type=authorization_code&client_id=8998879&client_secret=iou0q9wud0q9wd0q9wid0q9iwd0q9wid0q9wdqwd&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&code=invalidcode") 78 | 79 | def test_can_authenticate_by_calling_auth_with_api_key(self): 80 | self.cs = CreateSend(self.api_key_auth_details) 81 | self.cs.stub_request("systemdate.json", "systemdate.json") 82 | systemdate = self.cs.systemdate() 83 | self.assertEqual(self.cs.headers['Authorization'], "Basic %s" % base64.b64encode( 84 | ("%s:x" % self.api_key_auth_details['api_key']).encode()).decode()) 85 | self.assertEqual(systemdate, "2010-10-15 09:27:00") 86 | 87 | def test_can_authenticate_by_calling_auth_with_oauth_credentials(self): 88 | self.cs = CreateSend(self.oauth_auth_details) 89 | self.cs.stub_request("systemdate.json", "systemdate.json") 90 | systemdate = self.cs.systemdate() 91 | self.assertEqual(self.cs.headers[ 92 | 'Authorization'], "Bearer %s" % self.oauth_auth_details['access_token']) 93 | self.assertEqual(systemdate, "2010-10-15 09:27:00") 94 | 95 | def test_raise_error_when_authenticating_with_oauth_and_token_expired(self): 96 | self.cs = CreateSend(self.oauth_auth_details) 97 | self.cs.stub_request( 98 | "systemdate.json", 'expired_oauth_token_api_error.json', status=401) 99 | self.assertRaises(ExpiredOAuthToken, self.cs.systemdate) 100 | 101 | def test_refresh_token(self): 102 | self.cs = CreateSend(self.oauth_auth_details) 103 | self.cs.stub_request( 104 | "https://api.createsend.com/oauth/token", "refresh_oauth_token.json") 105 | new_access_token, new_expires_in, new_refresh_token = self.cs.refresh_token() 106 | 107 | self.assertEqual(self.cs.faker.actual_body, 108 | "grant_type=refresh_token&refresh_token=5S4aASP9R%2B9KsgfHB0dapTYxNA%3D%3D") 109 | self.assertEqual(new_access_token, "SlAV32hkKG2e12e") 110 | self.assertEqual(new_expires_in, 1209600) 111 | self.assertEqual(new_refresh_token, "tGzv3JOkF0XG5Qx2TlKWIA") 112 | self.assertEqual(self.cs.auth_details, 113 | {'access_token': new_access_token, 'refresh_token': new_refresh_token}) 114 | 115 | def test_refresh_token_error_when_refresh_token_none(self): 116 | self.cs = CreateSend( 117 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": None}) 118 | self.assertRaises(Exception, self.cs.refresh_token) 119 | 120 | def test_refresh_token_error_when_no_refresh_token_passed_in(self): 121 | self.cs = CreateSend({"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA=="}) 122 | self.assertRaises(Exception, self.cs.refresh_token) 123 | 124 | def test_refresh_token_error_when_no_authentication(self): 125 | self.cs = CreateSend() 126 | self.assertRaises(Exception, self.cs.refresh_token) 127 | -------------------------------------------------------------------------------- /test/test_transactional.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from createsend.transactional import Transactional 4 | 5 | 6 | class TransactionalTestCase: 7 | 8 | def test_smart_email_list(self): 9 | status = "all" 10 | self.tx.stub_request("transactional/smartEmail?status=%s" % 11 | status, "tx_smartemails.json") 12 | list = self.tx.smart_email_list(status) 13 | self.assertEqual(list[0].Name, "Welcome email") 14 | 15 | def test_active_smart_email_list(self): 16 | self.tx.stub_request( 17 | "transactional/smartEmail?status=active", "tx_smartemails.json") 18 | list = self.tx.smart_email_list(status="active") 19 | self.assertEqual(list[0].Status, "Active") 20 | 21 | def test_smart_email_list_with_client(self): 22 | self.tx.stub_request("transactional/smartEmail?status=all&clientID=%s" % 23 | self.client_id, "tx_smartemails.json") 24 | list = self.tx.smart_email_list(client_id=self.client_id) 25 | self.assertEqual(list[0].Name, "Welcome email") 26 | 27 | def test_smart_email_details(self): 28 | self.tx.stub_request("transactional/smartEmail/%s" % 29 | self.smart_email_id, "tx_smartemail_details.json") 30 | email = self.tx.smart_email_details(self.smart_email_id) 31 | self.assertEqual(email.Name, "Reset Password") 32 | 33 | def test_smart_email_send_single(self): 34 | self.tx.stub_request("transactional/smartEmail/%s/send" % 35 | self.smart_email_id, "tx_send_single.json") 36 | send = self.tx.smart_email_send( 37 | self.smart_email_id, "\"Bob Sacamano\" ", "Yes") 38 | self.assertEqual(send[0].Status, "Received") 39 | 40 | def test_smart_email_send_multiple(self): 41 | self.tx.stub_request("transactional/smartEmail/%s/send" % 42 | self.smart_email_id, "tx_send_multiple.json") 43 | send = self.tx.smart_email_send(self.smart_email_id, [ 44 | "\"Bob Sacamano\" ", "\"Newman\" "], "No") 45 | self.assertEqual(send[1].Recipient, "\"Newman\" ") 46 | 47 | def test_classic_email_send(self): 48 | self.tx.stub_request( 49 | "transactional/classicEmail/send", "tx_send_single.json") 50 | send = self.tx.classic_email_send( 51 | "This is the subject", "from@example.com", "\"Bob Sacamano\" ", "Yes") 52 | self.assertEqual(send[0].Recipient, 53 | "\"Bob Sacamano\" ") 54 | 55 | def test_classic_email_groups(self): 56 | self.tx.stub_request( 57 | "transactional/classicEmail/groups", "tx_classicemail_groups.json") 58 | groups = self.tx.classic_email_groups() 59 | self.assertEqual(groups[0].Group, "Password Reset") 60 | 61 | def test_classic_email_groups_with_client(self): 62 | self.tx.stub_request("transactional/classicEmail/groups?clientID=%s" % 63 | self.client_id, "tx_classicemail_groups.json") 64 | groups = self.tx.classic_email_groups(client_id=self.client_id) 65 | self.assertEqual(groups[0].Group, "Password Reset") 66 | 67 | def test_statistics(self): 68 | self.tx.stub_request("transactional/statistics", 69 | "tx_statistics_classic.json") 70 | stats = self.tx.statistics() 71 | self.assertEqual(stats.Query.Group, "Password Reset") 72 | 73 | def test_statistics_with_options(self): 74 | start = "2014-02-03" 75 | end = "2015-02-02" 76 | self.tx.stub_request('transactional/statistics?to=%s&from=%s&clientID=%s' % 77 | (end, start, self.client_id), "tx_statistics_classic.json") 78 | stats = self.tx.statistics( 79 | {'clientID': self.client_id, 'from': start, 'to': end}) 80 | self.assertEqual(stats.Query.Group, "Password Reset") 81 | 82 | def test_timeline(self): 83 | self.tx.stub_request("transactional/messages", "tx_messages.json") 84 | timeline = self.tx.message_timeline() 85 | self.assertEqual(len(timeline), 3) 86 | self.assertEqual(timeline[0].Status, "Delivered") 87 | 88 | def test_timeline_classic_with_options(self): 89 | self.tx.stub_request('transactional/messages?status=%s&group=%s' % 90 | ("all", "Password+Reset"), "tx_messages_classic.json") 91 | timeline = self.tx.message_timeline( 92 | {'status': 'all', 'group': 'Password Reset'}) 93 | self.assertEqual(timeline[0].Group, "Password Reset") 94 | 95 | def test_timeline_smart_with_options(self): 96 | self.tx.stub_request('transactional/messages?status=%s&smartEmailID=%s' % 97 | ("all", self.smart_email_id), "tx_messages_smart.json") 98 | timeline = self.tx.message_timeline( 99 | {'status': 'all', 'smartEmailID': self.smart_email_id}) 100 | self.assertEqual(timeline[0].SmartEmailID, self.smart_email_id) 101 | 102 | def test_message_details(self): 103 | self.tx.stub_request('transactional/messages/%s?statistics=False&excludemessagebody=False' % 104 | (self.message_id), "tx_message_details.json") 105 | msg = self.tx.message_details(self.message_id, statistics=False, exclude_message_body=False) 106 | self.assertEqual(msg.MessageID, self.message_id) 107 | 108 | def test_message_details_with_stats(self): 109 | self.tx.stub_request('transactional/messages/%s?statistics=True&excludemessagebody=False' % 110 | (self.message_id), "tx_message_details_with_statistics.json") 111 | msg = self.tx.message_details(self.message_id, statistics=True, exclude_message_body=False) 112 | self.assertEqual(len(msg.Opens), 1) 113 | self.assertEqual(len(msg.Clicks), 1) 114 | 115 | def test_message_resend(self): 116 | self.tx.stub_request('transactional/messages/%s/resend' % 117 | (self.message_id), "tx_send_single.json") 118 | send = self.tx.message_resend(self.message_id) 119 | self.assertEqual(send[0].Status, "Received") 120 | 121 | 122 | class OAuthTransactionalTestCase(unittest.TestCase, TransactionalTestCase): 123 | """Test when using OAuth to authenticate""" 124 | 125 | def setUp(self): 126 | self.client_id = '87y8d7qyw8d7yq8w7ydwqwd' 127 | self.smart_email_id = '21dab350-f484-11e4-ad38-6c4008bc7468' 128 | self.message_id = 'ddc697c7-0788-4df3-a71a-a7cb935f00bd' 129 | self.before_id = 'e2e270e6-fbce-11e4-97fc-a7cf717ca157' 130 | self.after_id = 'e96fc6ca-fbce-11e4-949f-c3ccd6a68863' 131 | self.tx = Transactional( 132 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, "admin@example.com") 133 | 134 | 135 | class ApiKeyTransactionalTestCase(unittest.TestCase, TransactionalTestCase): 136 | """Test when using an API key to authenticate""" 137 | 138 | def setUp(self): 139 | self.client_id = '87y8d7qyw8d7yq8w7ydwqwd' 140 | self.smart_email_id = '21dab350-f484-11e4-ad38-6c4008bc7468' 141 | self.message_id = 'ddc697c7-0788-4df3-a71a-a7cb935f00bd' 142 | self.before_id = 'e2e270e6-fbce-11e4-97fc-a7cf717ca157' 143 | self.after_id = 'e96fc6ca-fbce-11e4-949f-c3ccd6a68863' 144 | self.tx = Transactional( 145 | {'api_key': '123123123123123123123'}, "admin@example.com") 146 | -------------------------------------------------------------------------------- /lib/createsend/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Client(CreateSendBase): 8 | """Represents a client and associated functionality.""" 9 | 10 | def __init__(self, auth=None, client_id=None): 11 | self.client_id = client_id 12 | super().__init__(auth) 13 | 14 | def create(self, company, timezone, country): 15 | """Creates a client.""" 16 | 17 | body = { 18 | "CompanyName": company, 19 | "TimeZone": timezone, 20 | "Country": country} 21 | response = self._post("/clients.json", json.dumps(body)) 22 | self.client_id = json_to_py(response) 23 | return self.client_id 24 | 25 | def details(self): 26 | """Gets the details of this client.""" 27 | response = self._get("/clients/%s.json" % self.client_id) 28 | return json_to_py(response) 29 | 30 | def campaigns(self, sent_from_date="", sent_to_date="", tags="", page=1, page_size=1000, order_direction="desc"): 31 | """Gets the sent campaigns belonging to this client.""" 32 | params = { 33 | "sentfromdate": sent_from_date, 34 | "senttodate": sent_to_date, 35 | "page": page, 36 | "tags": tags, 37 | "pagesize": page_size, 38 | "orderdirection": order_direction, 39 | } 40 | response = self._get(self.uri_for("campaigns"), params=params) 41 | return json_to_py(response) 42 | 43 | def scheduled(self): 44 | """Gets the currently scheduled campaigns belonging to this client.""" 45 | response = self._get(self.uri_for("scheduled")) 46 | return json_to_py(response) 47 | 48 | def drafts(self): 49 | """Gets the draft campaigns belonging to this client.""" 50 | response = self._get(self.uri_for("drafts")) 51 | return json_to_py(response) 52 | 53 | def tags(self): 54 | """Gets the list of tags belonging to this client.""" 55 | response = self._get(self.uri_for("tags")) 56 | return json_to_py(response) 57 | 58 | def lists(self): 59 | """Gets the subscriber lists belonging to this client.""" 60 | response = self._get(self.uri_for("lists")) 61 | return json_to_py(response) 62 | 63 | def lists_for_email(self, email_address): 64 | """Gets the lists across a client to which a subscriber with a particular 65 | email address belongs.""" 66 | params = {"email": email_address} 67 | response = self._get(self.uri_for("listsforemail"), params=params) 68 | return json_to_py(response) 69 | 70 | def segments(self): 71 | """Gets the segments belonging to this client.""" 72 | response = self._get(self.uri_for("segments")) 73 | return json_to_py(response) 74 | 75 | def suppressionlist(self, page=1, page_size=1000, order_field="email", order_direction="asc"): 76 | """Gets this client's suppression list.""" 77 | params = { 78 | "page": page, 79 | "pagesize": page_size, 80 | "orderfield": order_field, 81 | "orderdirection": order_direction} 82 | response = self._get(self.uri_for("suppressionlist"), params=params) 83 | return json_to_py(response) 84 | 85 | def suppress(self, email): 86 | """Adds email addresses to a client's suppression list""" 87 | body = { 88 | "EmailAddresses": [email] if isinstance(email, str) else email} 89 | response = self._post(self.uri_for("suppress"), json.dumps(body)) 90 | 91 | def unsuppress(self, email): 92 | """Unsuppresses an email address by removing it from the the client's 93 | suppression list""" 94 | params = {"email": email} 95 | response = self._put(self.uri_for("unsuppress"), 96 | body=" ", params=params) 97 | 98 | def templates(self): 99 | """Gets the templates belonging to this client.""" 100 | response = self._get(self.uri_for("templates")) 101 | return json_to_py(response) 102 | 103 | def set_basics(self, company, timezone, country): 104 | body = { 105 | "CompanyName": company, 106 | "TimeZone": timezone, 107 | "Country": country} 108 | response = self._put(self.uri_for('setbasics'), json.dumps(body)) 109 | 110 | def set_payg_billing(self, currency, can_purchase_credits, client_pays, markup_percentage, 111 | markup_on_delivery=0, markup_per_recipient=0): 112 | """Sets the PAYG billing settings for this client.""" 113 | body = { 114 | "Currency": currency, 115 | "CanPurchaseCredits": can_purchase_credits, 116 | "ClientPays": client_pays, 117 | "MarkupPercentage": markup_percentage, 118 | "MarkupOnDelivery": markup_on_delivery, 119 | "MarkupPerRecipient": markup_per_recipient} 120 | response = self._put(self.uri_for('setpaygbilling'), json.dumps(body)) 121 | 122 | def set_monthly_billing(self, currency, client_pays, markup_percentage, monthly_scheme=None): 123 | """Sets the monthly billing settings for this client.""" 124 | body = { 125 | "Currency": currency, 126 | "ClientPays": client_pays, 127 | "MarkupPercentage": markup_percentage} 128 | 129 | if monthly_scheme is not None: 130 | body["MonthlyScheme"] = monthly_scheme 131 | 132 | response = self._put(self.uri_for( 133 | 'setmonthlybilling'), json.dumps(body)) 134 | 135 | def transfer_credits(self, credits, can_use_my_credits_when_they_run_out): 136 | """Transfer credits to or from this client. 137 | 138 | :param credits: An Integer representing the number of credits to transfer. 139 | This value may be either positive if you want to allocate credits from 140 | your account to the client, or negative if you want to deduct credits 141 | from the client back into your account. 142 | :param can_use_my_credits_when_they_run_out: A Boolean value representing 143 | which, if set to true, will allow the client to continue sending using 144 | your credits or payment details once they run out of credits, and if 145 | set to false, will prevent the client from using your credits to 146 | continue sending until you allocate more credits to them. 147 | :returns: An object of the following form representing the result: 148 | { 149 | AccountCredits # Integer representing credits in your account now 150 | ClientCredits # Integer representing credits in this client's 151 | account now 152 | } 153 | """ 154 | body = { 155 | "Credits": credits, 156 | "CanUseMyCreditsWhenTheyRunOut": can_use_my_credits_when_they_run_out} 157 | response = self._post(self.uri_for('credits'), json.dumps(body)) 158 | return json_to_py(response) 159 | 160 | def people(self): 161 | """gets people associated with the client""" 162 | response = self._get(self.uri_for('people')) 163 | return json_to_py(response) 164 | 165 | def get_primary_contact(self): 166 | """retrieves the primary contact for this client""" 167 | response = self._get(self.uri_for('primarycontact')) 168 | return json_to_py(response) 169 | 170 | def set_primary_contact(self, email): 171 | """assigns the primary contact for this client""" 172 | params = {"email": email} 173 | response = self._put(self.uri_for('primarycontact'), params=params) 174 | return json_to_py(response) 175 | 176 | def delete(self): 177 | """Deletes this client.""" 178 | response = self._delete("/clients/%s.json" % self.client_id) 179 | 180 | def journeys(self): 181 | """Retrieves the journeys associated with the client""" 182 | response = self._get(self.uri_for('journeys')) 183 | return json_to_py(response) 184 | 185 | def uri_for(self, action): 186 | return f"/clients/{self.client_id}/{action}.json" 187 | -------------------------------------------------------------------------------- /lib/createsend/campaign.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from createsend.createsend import CreateSendBase 4 | from createsend.utils import json_to_py 5 | 6 | 7 | class Campaign(CreateSendBase): 8 | """Represents a campaign and provides associated functionality.""" 9 | 10 | def __init__(self, auth=None, campaign_id=None): 11 | self.campaign_id = campaign_id 12 | super().__init__(auth) 13 | 14 | def create(self, client_id, subject, name, from_name, from_email, reply_to, html_url, 15 | text_url, list_ids, segment_ids): 16 | """Creates a new campaign for a client. 17 | 18 | :param client_id: String representing the ID of the client for whom the 19 | campaign will be created. 20 | :param subject: String representing the subject of the campaign. 21 | :param name: String representing the name of the campaign. 22 | :param from_name: String representing the from name for the campaign. 23 | :param from_email: String representing the from address for the campaign. 24 | :param reply_to: String representing the reply-to address for the campaign. 25 | :param html_url: String representing the URL for the campaign HTML content. 26 | :param text_url: String representing the URL for the campaign text content. 27 | Note that text_url is optional and if None or an empty string, text 28 | content will be automatically generated from the HTML content. 29 | :param list_ids: Array of Strings representing the IDs of the lists to 30 | which the campaign will be sent. 31 | :param segment_ids: Array of Strings representing the IDs of the segments to 32 | which the campaign will be sent. 33 | :returns String representing the ID of the newly created campaign. 34 | """ 35 | body = { 36 | "Subject": subject, 37 | "Name": name, 38 | "FromName": from_name, 39 | "FromEmail": from_email, 40 | "ReplyTo": reply_to, 41 | "HtmlUrl": html_url, 42 | "TextUrl": text_url, 43 | "ListIDs": list_ids, 44 | "SegmentIDs": segment_ids} 45 | response = self._post("/campaigns/%s.json" % 46 | client_id, json.dumps(body)) 47 | self.campaign_id = json_to_py(response) 48 | return self.campaign_id 49 | 50 | def create_from_template(self, client_id, subject, name, from_name, 51 | from_email, reply_to, list_ids, segment_ids, template_id, template_content): 52 | """Creates a new campaign for a client, from a template. 53 | 54 | :param client_id: String representing the ID of the client for whom the 55 | campaign will be created. 56 | :param subject: String representing the subject of the campaign. 57 | :param name: String representing the name of the campaign. 58 | :param from_name: String representing the from name for the campaign. 59 | :param from_email: String representing the from address for the campaign. 60 | :param reply_to: String representing the reply-to address for the campaign. 61 | :param list_ids: Array of Strings representing the IDs of the lists to 62 | which the campaign will be sent. 63 | :param segment_ids: Array of Strings representing the IDs of the segments to 64 | which the campaign will be sent. 65 | :param template_id: String representing the ID of the template on which 66 | the campaign will be based. 67 | :param template_content: Hash representing the content to be used for the 68 | editable areas of the template. See documentation at 69 | campaignmonitor.com/api/campaigns/#creating_a_campaign_from_template 70 | for full details of template content format. 71 | :returns String representing the ID of the newly created campaign. 72 | """ 73 | body = { 74 | "Subject": subject, 75 | "Name": name, 76 | "FromName": from_name, 77 | "FromEmail": from_email, 78 | "ReplyTo": reply_to, 79 | "ListIDs": list_ids, 80 | "SegmentIDs": segment_ids, 81 | "TemplateID": template_id, 82 | "TemplateContent": template_content} 83 | response = self._post("/campaigns/%s/fromtemplate.json" % 84 | client_id, json.dumps(body)) 85 | self.campaign_id = json_to_py(response) 86 | return self.campaign_id 87 | 88 | def send_preview(self, recipients, personalize="fallback"): 89 | """Sends a preview of this campaign.""" 90 | body = { 91 | "PreviewRecipients": [recipients] if isinstance(recipients, str) else recipients, 92 | "Personalize": personalize} 93 | response = self._post(self.uri_for("sendpreview"), json.dumps(body)) 94 | 95 | def send(self, confirmation_email, send_date="immediately"): 96 | """Sends this campaign.""" 97 | body = { 98 | "ConfirmationEmail": confirmation_email, 99 | "SendDate": send_date} 100 | response = self._post(self.uri_for("send"), json.dumps(body)) 101 | 102 | def unschedule(self): 103 | """Unschedules this campaign if it is currently scheduled.""" 104 | response = self._post(self.uri_for("unschedule"), json.dumps({})) 105 | 106 | def delete(self): 107 | """Deletes this campaign.""" 108 | response = self._delete("/campaigns/%s.json" % self.campaign_id) 109 | 110 | def summary(self): 111 | """Gets a summary of this campaign""" 112 | response = self._get(self.uri_for("summary")) 113 | return json_to_py(response) 114 | 115 | def email_client_usage(self): 116 | """Gets the email clients that subscribers used to open the campaign""" 117 | response = self._get(self.uri_for("emailclientusage")) 118 | return json_to_py(response) 119 | 120 | def lists_and_segments(self): 121 | """Retrieves the lists and segments to which this campaaign will be (or was) sent.""" 122 | response = self._get(self.uri_for("listsandsegments")) 123 | return json_to_py(response) 124 | 125 | def recipients(self, page=1, page_size=1000, order_field="email", order_direction="asc"): 126 | """Retrieves the recipients of this campaign.""" 127 | params = { 128 | "page": page, 129 | "pagesize": page_size, 130 | "orderfield": order_field, 131 | "orderdirection": order_direction} 132 | response = self._get(self.uri_for("recipients"), params=params) 133 | return json_to_py(response) 134 | 135 | def opens(self, date="", page=1, page_size=1000, order_field="date", order_direction="asc"): 136 | """Retrieves the opens for this campaign.""" 137 | params = { 138 | "date": date, 139 | "page": page, 140 | "pagesize": page_size, 141 | "orderfield": order_field, 142 | "orderdirection": order_direction} 143 | response = self._get(self.uri_for("opens"), params=params) 144 | return json_to_py(response) 145 | 146 | def clicks(self, date="", page=1, page_size=1000, order_field="date", order_direction="asc"): 147 | """Retrieves the subscriber clicks for this campaign.""" 148 | params = { 149 | "date": date, 150 | "page": page, 151 | "pagesize": page_size, 152 | "orderfield": order_field, 153 | "orderdirection": order_direction} 154 | response = self._get(self.uri_for("clicks"), params=params) 155 | return json_to_py(response) 156 | 157 | def unsubscribes(self, date="", page=1, page_size=1000, order_field="date", order_direction="asc"): 158 | """Retrieves the unsubscribes for this campaign.""" 159 | params = { 160 | "date": date, 161 | "page": page, 162 | "pagesize": page_size, 163 | "orderfield": order_field, 164 | "orderdirection": order_direction} 165 | response = self._get(self.uri_for("unsubscribes"), params=params) 166 | return json_to_py(response) 167 | 168 | def spam(self, date="", page=1, page_size=1000, order_field="date", order_direction="asc"): 169 | """Retrieves the spam complaints for this campaign.""" 170 | params = { 171 | "date": date, 172 | "page": page, 173 | "pagesize": page_size, 174 | "orderfield": order_field, 175 | "orderdirection": order_direction} 176 | response = self._get(self.uri_for("spam"), params=params) 177 | return json_to_py(response) 178 | 179 | def bounces(self, date="", page=1, page_size=1000, order_field="date", order_direction="asc"): 180 | """Retrieves the bounces for this campaign.""" 181 | params = { 182 | "date": date, 183 | "page": page, 184 | "pagesize": page_size, 185 | "orderfield": order_field, 186 | "orderdirection": order_direction} 187 | response = self._get(self.uri_for("bounces"), params=params) 188 | return json_to_py(response) 189 | 190 | def uri_for(self, action): 191 | return f"/campaigns/{self.campaign_id}/{action}.json" 192 | -------------------------------------------------------------------------------- /lib/createsend/list.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import quote 3 | 4 | from createsend.createsend import CreateSendBase 5 | from createsend.utils import json_to_py 6 | 7 | 8 | class List(CreateSendBase): 9 | """Represents a subscriber list and associated functionality.""" 10 | 11 | def __init__(self, auth=None, list_id=None): 12 | self.list_id = list_id 13 | super().__init__(auth) 14 | 15 | def create(self, client_id, title, unsubscribe_page, confirmed_opt_in, 16 | confirmation_success_page, unsubscribe_setting="AllClientLists"): 17 | """Creates a new list for a client.""" 18 | body = { 19 | "Title": title, 20 | "UnsubscribePage": unsubscribe_page, 21 | "ConfirmedOptIn": confirmed_opt_in, 22 | "ConfirmationSuccessPage": confirmation_success_page, 23 | "UnsubscribeSetting": unsubscribe_setting} 24 | response = self._post("/lists/%s.json" % client_id, json.dumps(body)) 25 | self.list_id = json_to_py(response) 26 | return self.list_id 27 | 28 | def delete(self): 29 | """Deletes this list.""" 30 | response = self._delete("/lists/%s.json" % self.list_id) 31 | 32 | def create_custom_field(self, field_name, data_type, options=[], 33 | visible_in_preference_center=True): 34 | """Creates a new custom field for this list.""" 35 | body = { 36 | "FieldName": field_name, 37 | "DataType": data_type, 38 | "Options": options, 39 | "VisibleInPreferenceCenter": visible_in_preference_center} 40 | response = self._post(self.uri_for("customfields"), json.dumps(body)) 41 | return json_to_py(response) 42 | 43 | def update_custom_field(self, custom_field_key, field_name, 44 | visible_in_preference_center): 45 | """Updates a custom field belonging to this list.""" 46 | custom_field_key = quote(custom_field_key, '') 47 | body = { 48 | "FieldName": field_name, 49 | "VisibleInPreferenceCenter": visible_in_preference_center} 50 | response = self._put(self.uri_for("customfields/%s" % 51 | custom_field_key), json.dumps(body)) 52 | return json_to_py(response) 53 | 54 | def delete_custom_field(self, custom_field_key): 55 | """Deletes a custom field associated with this list.""" 56 | custom_field_key = quote(custom_field_key, '') 57 | response = self._delete("/lists/%s/customfields/%s.json" % 58 | (self.list_id, custom_field_key)) 59 | 60 | def update_custom_field_options(self, custom_field_key, new_options, 61 | keep_existing_options): 62 | """Updates the options of a multi-optioned custom field on this list.""" 63 | custom_field_key = quote(custom_field_key, '') 64 | body = { 65 | "Options": new_options, 66 | "KeepExistingOptions": keep_existing_options} 67 | response = self._put(self.uri_for( 68 | "customfields/%s/options" % custom_field_key), json.dumps(body)) 69 | 70 | def details(self): 71 | """Gets the details of this list.""" 72 | response = self._get("/lists/%s.json" % self.list_id) 73 | return json_to_py(response) 74 | 75 | def custom_fields(self): 76 | """Gets the custom fields for this list.""" 77 | response = self._get(self.uri_for("customfields")) 78 | return json_to_py(response) 79 | 80 | def segments(self): 81 | """Gets the segments for this list.""" 82 | response = self._get(self.uri_for("segments")) 83 | return json_to_py(response) 84 | 85 | def stats(self): 86 | """Gets the stats for this list.""" 87 | response = self._get(self.uri_for("stats")) 88 | return json_to_py(response) 89 | 90 | def active(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_preference=False): 91 | """Gets the active subscribers for this list.""" 92 | params = { 93 | "date": date, 94 | "page": page, 95 | "pagesize": page_size, 96 | "orderfield": order_field, 97 | "orderdirection": order_direction, 98 | "includetrackingpreference": include_tracking_preference, 99 | } 100 | response = self._get(self.uri_for("active"), params=params) 101 | return json_to_py(response) 102 | 103 | def unconfirmed(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_preference=False): 104 | """Gets the unconfirmed subscribers for this list.""" 105 | params = { 106 | "date": date, 107 | "page": page, 108 | "pagesize": page_size, 109 | "orderfield": order_field, 110 | "orderdirection": order_direction, 111 | "includetrackingpreference": include_tracking_preference, 112 | } 113 | response = self._get(self.uri_for("unconfirmed"), params=params) 114 | return json_to_py(response) 115 | 116 | def bounced(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_preference=False): 117 | """Gets the bounced subscribers for this list.""" 118 | params = { 119 | "date": date, 120 | "page": page, 121 | "pagesize": page_size, 122 | "orderfield": order_field, 123 | "orderdirection": order_direction, 124 | "includetrackingpreference": include_tracking_preference, 125 | } 126 | response = self._get(self.uri_for("bounced"), params=params) 127 | return json_to_py(response) 128 | 129 | def unsubscribed(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_preference=False): 130 | """Gets the unsubscribed subscribers for this list.""" 131 | params = { 132 | "date": date, 133 | "page": page, 134 | "pagesize": page_size, 135 | "orderfield": order_field, 136 | "orderdirection": order_direction, 137 | "includetrackingpreference": include_tracking_preference, 138 | } 139 | response = self._get(self.uri_for("unsubscribed"), params=params) 140 | return json_to_py(response) 141 | 142 | def deleted(self, date="", page=1, page_size=1000, order_field="email", order_direction="asc", include_tracking_preference=False): 143 | """Gets the deleted subscribers for this list.""" 144 | params = { 145 | "date": date, 146 | "page": page, 147 | "pagesize": page_size, 148 | "orderfield": order_field, 149 | "orderdirection": order_direction, 150 | "includetrackingpreference": include_tracking_preference, 151 | } 152 | response = self._get(self.uri_for("deleted"), params=params) 153 | return json_to_py(response) 154 | 155 | def update(self, title, unsubscribe_page, confirmed_opt_in, 156 | confirmation_success_page, unsubscribe_setting="AllClientLists", 157 | add_unsubscribes_to_supp_list=False, scrub_active_with_supp_list=False): 158 | """Updates this list.""" 159 | body = { 160 | "Title": title, 161 | "UnsubscribePage": unsubscribe_page, 162 | "ConfirmedOptIn": confirmed_opt_in, 163 | "ConfirmationSuccessPage": confirmation_success_page, 164 | "UnsubscribeSetting": unsubscribe_setting, 165 | "AddUnsubscribesToSuppList": add_unsubscribes_to_supp_list, 166 | "ScrubActiveWithSuppList": scrub_active_with_supp_list} 167 | response = self._put("/lists/%s.json" % self.list_id, json.dumps(body)) 168 | 169 | def webhooks(self): 170 | """Gets the webhooks for this list.""" 171 | response = self._get(self.uri_for("webhooks")) 172 | return json_to_py(response) 173 | 174 | def create_webhook(self, events, url, payload_format): 175 | """Creates a new webhook for the specified events (an array of strings). 176 | Valid events are "Subscribe", "Deactivate", and "Update". 177 | Valid payload formats are "json", and "xml".""" 178 | body = { 179 | "Events": events, 180 | "Url": url, 181 | "PayloadFormat": payload_format} 182 | response = self._post(self.uri_for("webhooks"), json.dumps(body)) 183 | return json_to_py(response) 184 | 185 | def test_webhook(self, webhook_id): 186 | """Tests that a post can be made to the endpoint specified for the webhook 187 | identified by webhook_id.""" 188 | response = self._get(self.uri_for("webhooks/%s/test" % webhook_id)) 189 | return True # An exception will be raised if any error occurs 190 | 191 | def delete_webhook(self, webhook_id): 192 | """Deletes a webhook associated with this list.""" 193 | response = self._delete("/lists/%s/webhooks/%s.json" % 194 | (self.list_id, webhook_id)) 195 | 196 | def activate_webhook(self, webhook_id): 197 | """Activates a webhook associated with this list.""" 198 | response = self._put(self.uri_for( 199 | "webhooks/%s/activate" % webhook_id), ' ') 200 | 201 | def deactivate_webhook(self, webhook_id): 202 | """De-activates a webhook associated with this list.""" 203 | response = self._put(self.uri_for( 204 | "webhooks/%s/deactivate" % webhook_id), ' ') 205 | 206 | def uri_for(self, action): 207 | return f"/lists/{self.list_id}/{action}.json" 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # createsend 2 | 3 | A Python library which implements the complete functionality of the [Campaign Monitor API](https://www.campaignmonitor.com/api/). Requires Python 3.8 or above. 4 | 5 | ## Installation 6 | 7 | ``` 8 | pip install createsend 9 | ``` 10 | 11 | ## Authenticating 12 | 13 | The Campaign Monitor API supports authentication using either OAuth or an API key. 14 | 15 | ### Using OAuth 16 | 17 | Depending on the environment you are developing in, you may wish to use a Python OAuth library to get access tokens for your users. If you use [Flask](https://flask.palletsprojects.com/en/stable/), you may like to refer to this [example application](https://gist.github.com/jdennes/4754097), which uses the [Flask-OAuth](https://pythonhosted.org/Flask-OAuth/) package to authenticate. 18 | 19 | If you don't use an OAuth library, you will need to manually get access tokens for your users by following the instructions included in the Campaign Monitor API [documentation](https://www.campaignmonitor.com/api/v3-3/getting-started/#authentication). This package provides functionality to help you do this, as described below. There's also another Flask [example application](https://gist.github.com/jdennes/4761254) you may wish to reference, which doesn't depend on any OAuth libraries. 20 | 21 | The first thing your application should do is redirect your user to the Campaign Monitor authorization URL where they will have the opportunity to approve your application to access their Campaign Monitor account. You can get this authorization URL by using the `authorize_url()` function, like so: 22 | 23 | ```python 24 | from createsend import * 25 | 26 | cs = CreateSend() 27 | authorize_url = cs.authorize_url( 28 | client_id='Client ID for your application', 29 | redirect_uri='Redirect URI for your application', 30 | scope='The permission level your application requires', 31 | state='Optional state data to be included' 32 | ) 33 | # Redirect your users to authorize_url. 34 | ``` 35 | 36 | If your user approves your application, they will then be redirected to the `redirect_uri` you specified, which will include a `code` parameter, and optionally a `state` parameter in the query string. Your application should implement a handler which can exchange the code passed to it for an access token, using the `exchange_token()` function like so: 37 | 38 | ```python 39 | from createsend import * 40 | 41 | cs = CreateSend() 42 | access_token, expires_in, refresh_token = cs.exchange_token( 43 | client_id='Client ID for your application', 44 | client_secret='Client Secret for your application', 45 | redirect_uri='Redirect URI for your application', 46 | code='A unique code for your user' # Get the code parameter from the query string 47 | ) 48 | # Save access_token, expires_in, and refresh_token. 49 | ``` 50 | 51 | At this point you have an access token and refresh token for your user which you should store somewhere convenient so that your application can look up these values when your user wants to make future Campaign Monitor API calls. 52 | 53 | Once you have an access token and refresh token for your user, you can authenticate and make further API calls like so: 54 | 55 | ```python 56 | from createsend import * 57 | 58 | cs = CreateSend({ 59 | 'access_token': 'your access token', 60 | 'refresh_token': 'your refresh token' }) 61 | clients = cs.clients() 62 | ``` 63 | 64 | All OAuth tokens have an expiry time, and can be renewed with a corresponding refresh token. If your access token expires when attempting to make an API call, the `ExpiredOAuthToken` exception will be raised, so your code should handle this. Here's an example of how you could do this: 65 | 66 | ```python 67 | from createsend import * 68 | 69 | try: 70 | cs = CreateSend({ 71 | 'access_token': 'your access token', 72 | 'refresh_token': 'your refresh token' }) 73 | clients = cs.clients() 74 | except ExpiredOAuthToken as eot: 75 | access_token, expires_in, refresh_token = cs.refresh_token() 76 | # Save your updated access_token, expires_in, and refresh_token. 77 | clients = cs.clients() 78 | except Exception as e: 79 | print("Error: %s" % e) 80 | ``` 81 | 82 | ### Using an API key 83 | 84 | ```python 85 | from createsend import * 86 | 87 | cs = CreateSend({'api_key': 'your api key'}) 88 | clients = cs.clients() 89 | ``` 90 | 91 | ## Basic usage 92 | This example of listing all your clients and their draft campaigns demonstrates basic usage of the library and the data returned from the API: 93 | 94 | ```python 95 | from createsend import * 96 | 97 | auth = { 98 | 'access_token': 'your access token', 99 | 'refresh_token': 'your refresh token' } 100 | cs = CreateSend(auth) 101 | clients = cs.clients() 102 | 103 | for cl in clients: 104 | print("Client: %s" % cl.Name) 105 | client = Client(auth, cl.ClientID) 106 | print("- Campaigns:") 107 | for cm in client.drafts(): 108 | print(" - %s" % cm.Subject) 109 | ``` 110 | 111 | Running this example will result in something like: 112 | 113 | ``` 114 | Client: First Client 115 | - Campaigns: 116 | - Newsletter Number One 117 | - Newsletter Number Two 118 | Client: Second Client 119 | - Campaigns: 120 | - News for January 2013 121 | ``` 122 | 123 | ## Transactional 124 | 125 | Sample code that uses Transactional message detail and timeline endpoint API. 126 | ```python 127 | from createsend import Transactional 128 | import os 129 | import sys 130 | 131 | auth = {'api_key': os.getenv('CREATESEND_API_KEY', '')} 132 | msg_id = os.getenv('MESSAGE_ID', '') 133 | 134 | if len(auth) == 0: 135 | print("API Key Not Provided") 136 | sys.exit(1) 137 | 138 | if len(msg_id) == 0: 139 | print("Message ID Not Provided") 140 | sys.exit(1) 141 | 142 | #auth = {'api_key': '[api_key]'} 143 | #msg_id = "[message id]" # e.g., becd8473-6a19-1feb-84c5-28d16948a5fc 144 | 145 | tx = Transactional(auth) 146 | 147 | # Get message details using message id. 148 | # We can optionally disable loading the body by setting exclude_message_body to `True`. 149 | msg_details = tx.message_details(msg_id, statistics=False, exclude_message_body=True) 150 | print(f'smart email id: {msg_details.SmartEmailID}') 151 | print(f'bounce type: {msg_details.BounceType}') 152 | print(f'bounce category: {msg_details.BounceCategory}') 153 | print(f'html: {msg_details.Message.Body.Html}') 154 | print('--') 155 | 156 | # Count the number of bounced mail using message timeline 157 | msg_timeline = tx.message_timeline() 158 | num_bounced = 0 159 | for m in msg_timeline: 160 | print('--') 161 | print(f'message id: {m.MessageID}') 162 | if str.lower(m.Status) == 'bounced': 163 | num_bounced += 1 164 | print(f'bounce type: {m.BounceType}') 165 | print(f'bounce category: {m.BounceCategory}') 166 | print('--') 167 | print(f"total bounces: {num_bounced}") 168 | ``` 169 | 170 | ## Handling errors 171 | If the Campaign Monitor API returns an error, an exception will be raised. For example, if you attempt to create a campaign and enter empty values for subject and other required fields: 172 | 173 | ```python 174 | from createsend import * 175 | 176 | campaign = Campaign({ 177 | 'access_token': 'your access token', 178 | 'refresh_token': 'your refresh token' }) 179 | 180 | try: 181 | id = campaign.create("4a397ccaaa55eb4e6aa1221e1e2d7122", "", "", "", "", "", "", "", [], []) 182 | print("New campaign ID: %s" % id) 183 | except BadRequest as br: 184 | print("Bad request error: %s" % br) 185 | print("Error Code: %s" % br.data.Code) 186 | print("Error Message: %s" % br.data.Message) 187 | except Exception as e: 188 | print("Error: %s" % e) 189 | ``` 190 | 191 | Running this example will result in: 192 | 193 | ``` 194 | Bad request error: The CreateSend API responded with the following error - 304: Campaign Subject Required 195 | Error Code: 304 196 | Error Message: Campaign Subject Required 197 | ``` 198 | 199 | ## Expected input and output 200 | The best way of finding out the expected input and output of a particular method in a particular class is to use the unit tests as a reference. 201 | 202 | For example, if you wanted to find out how to call the `Subscriber.add()` method, you would look at the file [test/test_subscriber.py](https://github.com/campaignmonitor/createsend-python/blob/master/test/test_subscriber.py) 203 | 204 | ```python 205 | def test_add_with_custom_fields(self): 206 | self.subscriber.stub_request("subscribers/%s.json" % self.list_id, "add_subscriber.json") 207 | custom_fields = [ { "Key": 'website', "Value": 'https://example.com/' } ] 208 | email_address = self.subscriber.add(self.list_id, "subscriber@example.com", "Subscriber", custom_fields, True) 209 | self.assertEqual(email_address, "subscriber@example.com") 210 | ``` 211 | 212 | ## Running unit tests 213 | 214 | Here are some commands to help run the unit tests: 215 | ``` 216 | > python -m venv venv 217 | > source venv/bin/activate # On Windows, use: venv\Scripts\activate 218 | > pip install pytest 219 | > export PYTHONPATH=$(pwd)/lib # on Windows: set PYTHONPATH=%cd%\lib 220 | > pytest 221 | > # To run a specific test file 222 | > pytest test/test_administrator.py 223 | ``` 224 | 225 | To deactivate the virtual environment run: 226 | ``` 227 | > deactivate 228 | ``` 229 | 230 | ## Automated testing with tox 231 | 232 | There is some testing available to test this wrapper against different versions of Python with [tox](https://tox.wiki/). 233 | 234 | Here are the commands to get that running: 235 | ``` 236 | > pip install tox 237 | > tox 238 | ``` 239 | 240 | ## Contributing 241 | 242 | Please check the [guidelines for contributing](https://github.com/campaignmonitor/createsend-python/blob/master/CONTRIBUTING.md) to this repository. 243 | 244 | ## Releasing 245 | 246 | Please check the [instructions for releasing](https://github.com/campaignmonitor/createsend-python/blob/master/RELEASE.md) the `createsend` package. 247 | 248 | ## This stuff should be green 249 | 250 | [![Python tests](https://github.com/campaignmonitor/createsend-python/actions/workflows/tests.yml/badge.svg)](https://github.com/campaignmonitor/createsend-python/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/campaignmonitor/createsend-python/badge.png?branch=master)][coveralls] 251 | 252 | [coveralls]: https://coveralls.io/r/campaignmonitor/createsend-python 253 | -------------------------------------------------------------------------------- /test/test_journey_email.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import unittest 3 | 4 | from createsend.journey_email import JourneyEmail 5 | 6 | 7 | class JourneyEmailTestCase: 8 | 9 | def test_bounces_no_params(self): 10 | self.journey_email.stub_request(self.no_param_uri_for("bounces"), "journey_email_bounces_no_params.json") 11 | bounces = self.journey_email.bounces() 12 | self.assertEqual(len(bounces.Results), 2) 13 | bounce_one = bounces.Results[0] 14 | self.assertEqual(bounce_one.EmailAddress, "asdf@softbouncemyemail.comX") 15 | self.assertEqual(bounce_one.BounceType, "Soft") 16 | self.assertEqual(bounce_one.Date, "2019-08-20 14:24:00") 17 | self.assertEqual(bounce_one.Reason, "Soft Bounce - Dns Failure") 18 | self.assertEqual(bounces.ResultsOrderedBy, "Date") 19 | self.assertEqual(bounces.OrderDirection, "ASC") 20 | self.assertEqual(bounces.PageNumber, 1) 21 | self.assertEqual(bounces.PageSize, 1000) 22 | self.assertEqual(bounces.RecordsOnThisPage, 2) 23 | self.assertEqual(bounces.TotalNumberOfRecords, 2) 24 | self.assertEqual(bounces.NumberOfPages, 1) 25 | 26 | def test_bounces_with_params(self): 27 | self.journey_email.stub_request(self.param_uri_for("bounces", "2019-01-01", 1, 10, "desc"), "journey_email_bounces_with_params.json") 28 | bounces = self.journey_email.bounces(date="2019-01-01", page=1, page_size=10, order_direction="desc") 29 | self.assertEqual(len(bounces.Results), 2) 30 | bounce_one = bounces.Results[0] 31 | self.assertEqual(bounce_one.EmailAddress, "asdf@hardbouncemyemail.com") 32 | self.assertEqual(bounce_one.BounceType, "Hard") 33 | self.assertEqual(bounce_one.Date, "2019-08-21 04:26:00") 34 | self.assertEqual(bounce_one.Reason, "Hard Bounce") 35 | self.assertEqual(bounces.ResultsOrderedBy, "Date") 36 | self.assertEqual(bounces.OrderDirection, "DESC") 37 | self.assertEqual(bounces.PageNumber, 1) 38 | self.assertEqual(bounces.PageSize, 10) 39 | self.assertEqual(bounces.RecordsOnThisPage, 2) 40 | self.assertEqual(bounces.TotalNumberOfRecords, 2) 41 | self.assertEqual(bounces.NumberOfPages, 1) 42 | 43 | def test_clicks_no_params(self): 44 | self.journey_email.stub_request(self.no_param_uri_for("clicks"), "journey_email_clicks_no_params.json") 45 | clicks = self.journey_email.clicks() 46 | self.assertEqual(len(clicks.Results), 2) 47 | click_one = clicks.Results[0] 48 | self.assertEqual(click_one.EmailAddress, "asdf@example.com") 49 | self.assertEqual(click_one.Date, "2019-08-19 10:23:00") 50 | self.assertEqual(click_one.URL, "https://mail.google.com/mail/?hl=en&tab=wm") 51 | self.assertEqual(click_one.IPAddress, "198.148.196.144") 52 | self.assertEqual(click_one.Latitude, -33.8591) 53 | self.assertEqual(click_one.Longitude, 151.200195) 54 | self.assertEqual(click_one.City, "Sydney") 55 | self.assertEqual(click_one.Region, "New South Wales") 56 | self.assertEqual(click_one.CountryCode, "AU") 57 | self.assertEqual(click_one.CountryName, "Australia") 58 | self.assertEqual(clicks.ResultsOrderedBy, "Date") 59 | self.assertEqual(clicks.OrderDirection, "ASC") 60 | self.assertEqual(clicks.PageNumber, 1) 61 | self.assertEqual(clicks.PageSize, 1000) 62 | self.assertEqual(clicks.RecordsOnThisPage, 2) 63 | self.assertEqual(clicks.TotalNumberOfRecords, 2) 64 | self.assertEqual(clicks.NumberOfPages, 1) 65 | 66 | def test_clicks_with_params(self): 67 | self.journey_email.stub_request(self.param_uri_for("clicks", "2019-01-01", 1, 10, "desc"), "journey_email_clicks_with_params.json") 68 | clicks = self.journey_email.clicks(date="2019-01-01", page=1, page_size=10, order_direction="desc") 69 | self.assertEqual(len(clicks.Results), 2) 70 | click_one = clicks.Results[0] 71 | self.assertEqual(click_one.EmailAddress, "asdf+2@example.com") 72 | self.assertEqual(click_one.Date, "2019-08-19 10:24:00") 73 | self.assertEqual(click_one.URL, "https://example.com") 74 | self.assertEqual(click_one.IPAddress, "198.148.196.144") 75 | self.assertEqual(click_one.Latitude, -33.8591) 76 | self.assertEqual(click_one.Longitude, 151.200195) 77 | self.assertEqual(click_one.City, "Sydney") 78 | self.assertEqual(click_one.Region, "New South Wales") 79 | self.assertEqual(click_one.CountryCode, "AU") 80 | self.assertEqual(click_one.CountryName, "Australia") 81 | self.assertEqual(clicks.ResultsOrderedBy, "Date") 82 | self.assertEqual(clicks.OrderDirection, "DESC") 83 | self.assertEqual(clicks.PageNumber, 1) 84 | self.assertEqual(clicks.PageSize, 10) 85 | self.assertEqual(clicks.RecordsOnThisPage, 2) 86 | self.assertEqual(clicks.TotalNumberOfRecords, 2) 87 | self.assertEqual(clicks.NumberOfPages, 1) 88 | 89 | def test_opens_no_params(self): 90 | self.journey_email.stub_request(self.no_param_uri_for("opens"), "journey_email_opens_no_params.json") 91 | opens = self.journey_email.opens() 92 | self.assertEqual(len(opens.Results), 2) 93 | open_one = opens.Results[0] 94 | self.assertEqual(open_one.EmailAddress, "asdf@example.com") 95 | self.assertEqual(open_one.Date, "2019-08-19 10:23:00") 96 | self.assertEqual(open_one.IPAddress, "198.148.196.144") 97 | self.assertEqual(open_one.Latitude, -33.8591) 98 | self.assertEqual(open_one.Longitude, 151.200195) 99 | self.assertEqual(open_one.City, "Sydney") 100 | self.assertEqual(open_one.Region, "New South Wales") 101 | self.assertEqual(open_one.CountryCode, "AU") 102 | self.assertEqual(open_one.CountryName, "Australia") 103 | self.assertEqual(opens.ResultsOrderedBy, "Date") 104 | self.assertEqual(opens.OrderDirection, "ASC") 105 | self.assertEqual(opens.PageNumber, 1) 106 | self.assertEqual(opens.PageSize, 1000) 107 | self.assertEqual(opens.RecordsOnThisPage, 2) 108 | self.assertEqual(opens.TotalNumberOfRecords, 2) 109 | self.assertEqual(opens.NumberOfPages, 1) 110 | 111 | def test_opens_with_params(self): 112 | self.journey_email.stub_request(self.param_uri_for("opens", "2019-01-01", 1, 10, "desc"), "journey_email_opens_with_params.json") 113 | opens = self.journey_email.opens(date="2019-01-01", page=1, page_size=10, order_direction="desc") 114 | self.assertEqual(len(opens.Results), 2) 115 | open_one = opens.Results[0] 116 | self.assertEqual(open_one.EmailAddress, "asdf+2@example.com") 117 | self.assertEqual(open_one.Date, "2019-08-19 10:24:00") 118 | self.assertEqual(open_one.IPAddress, "198.148.196.144") 119 | self.assertEqual(open_one.Latitude, -33.8591) 120 | self.assertEqual(open_one.Longitude, 151.200195) 121 | self.assertEqual(open_one.City, "Sydney") 122 | self.assertEqual(open_one.Region, "New South Wales") 123 | self.assertEqual(open_one.CountryCode, "AU") 124 | self.assertEqual(open_one.CountryName, "Australia") 125 | self.assertEqual(opens.ResultsOrderedBy, "Date") 126 | self.assertEqual(opens.OrderDirection, "DESC") 127 | self.assertEqual(opens.PageNumber, 1) 128 | self.assertEqual(opens.PageSize, 10) 129 | self.assertEqual(opens.RecordsOnThisPage, 2) 130 | self.assertEqual(opens.TotalNumberOfRecords, 2) 131 | self.assertEqual(opens.NumberOfPages, 1) 132 | 133 | def test_recipients_no_params(self): 134 | self.journey_email.stub_request(self.no_param_uri_for("recipients"), "journey_email_recipients_no_params.json") 135 | recipients = self.journey_email.recipients() 136 | self.assertEqual(len(recipients.Results), 4) 137 | recipient_one = recipients.Results[0] 138 | self.assertEqual(recipient_one.EmailAddress, "asdf@example.com") 139 | self.assertEqual(recipient_one.SentDate, "2019-08-19 10:23:00") 140 | self.assertEqual(recipients.ResultsOrderedBy, "SentDate") 141 | self.assertEqual(recipients.OrderDirection, "ASC") 142 | self.assertEqual(recipients.PageNumber, 1) 143 | self.assertEqual(recipients.PageSize, 1000) 144 | self.assertEqual(recipients.RecordsOnThisPage, 4) 145 | self.assertEqual(recipients.TotalNumberOfRecords, 4) 146 | self.assertEqual(recipients.NumberOfPages, 1) 147 | 148 | def test_recipients_with_params(self): 149 | self.journey_email.stub_request(self.param_uri_for("recipients", "2019-01-01", 1, 10, "desc"), "journey_email_recipients_with_params.json") 150 | recipients = self.journey_email.recipients(date="2019-01-01", page=1, page_size=10, order_direction="desc") 151 | self.assertEqual(len(recipients.Results), 4) 152 | recipient_one = recipients.Results[0] 153 | self.assertEqual(recipient_one.EmailAddress, "asdf@hardbouncemyemail.com") 154 | self.assertEqual(recipient_one.SentDate, "2019-08-21 04:26:00") 155 | self.assertEqual(recipients.ResultsOrderedBy, "SentDate") 156 | self.assertEqual(recipients.OrderDirection, "DESC") 157 | self.assertEqual(recipients.PageNumber, 1) 158 | self.assertEqual(recipients.PageSize, 10) 159 | self.assertEqual(recipients.RecordsOnThisPage, 4) 160 | self.assertEqual(recipients.TotalNumberOfRecords, 4) 161 | self.assertEqual(recipients.NumberOfPages, 1) 162 | 163 | def test_unsubscribes_no_params(self): 164 | self.journey_email.stub_request(self.no_param_uri_for("unsubscribes"), "journey_email_unsubscribes_no_params.json") 165 | unsubscribes = self.journey_email.unsubscribes() 166 | self.assertEqual(len(unsubscribes.Results), 1) 167 | unsubscribe_one = unsubscribes.Results[0] 168 | self.assertEqual(unsubscribe_one.EmailAddress, "asdf@example.com") 169 | self.assertEqual(unsubscribe_one.Date, "2019-08-19 10:24:00") 170 | self.assertEqual(unsubscribe_one.IPAddress, "198.148.196.144") 171 | self.assertEqual(unsubscribes.ResultsOrderedBy, "Date") 172 | self.assertEqual(unsubscribes.OrderDirection, "ASC") 173 | self.assertEqual(unsubscribes.PageNumber, 1) 174 | self.assertEqual(unsubscribes.PageSize, 1000) 175 | self.assertEqual(unsubscribes.RecordsOnThisPage, 1) 176 | self.assertEqual(unsubscribes.TotalNumberOfRecords, 1) 177 | self.assertEqual(unsubscribes.NumberOfPages, 1) 178 | 179 | def test_unsubscribes_with_params(self): 180 | self.journey_email.stub_request(self.param_uri_for("unsubscribes", "2019-01-01", 1, 10, "desc"), "journey_email_unsubscribes_with_params.json") 181 | unsubscribes = self.journey_email.unsubscribes(date="2019-01-01", page=1, page_size=10, order_direction="desc") 182 | self.assertEqual(len(unsubscribes.Results), 1) 183 | unsubscribe_one = unsubscribes.Results[0] 184 | self.assertEqual(unsubscribe_one.EmailAddress, "asdf@example.com") 185 | self.assertEqual(unsubscribe_one.Date, "2019-08-19 10:24:00") 186 | self.assertEqual(unsubscribe_one.IPAddress, "198.148.196.144") 187 | self.assertEqual(unsubscribes.ResultsOrderedBy, "Date") 188 | self.assertEqual(unsubscribes.OrderDirection, "DESC") 189 | self.assertEqual(unsubscribes.PageNumber, 1) 190 | self.assertEqual(unsubscribes.PageSize, 10) 191 | self.assertEqual(unsubscribes.RecordsOnThisPage, 1) 192 | self.assertEqual(unsubscribes.TotalNumberOfRecords, 1) 193 | self.assertEqual(unsubscribes.NumberOfPages, 1) 194 | 195 | def no_param_uri_for(self, action): 196 | return "journeys/email/%s/%s.json" %\ 197 | (self.journey_email_id, action) 198 | 199 | def param_uri_for(self, action, date, page, pagesize, orderdirection): 200 | return "journeys/email/%s/%s.json?date=%s&page=%s&pagesize=%s&orderdirection=%s" %\ 201 | (self.journey_email_id, action, quote(date, ''), page, pagesize, orderdirection) 202 | 203 | 204 | class OAuthCampaignTestCase(unittest.TestCase, JourneyEmailTestCase): 205 | """Test when using OAuth to authenticate""" 206 | 207 | def setUp(self): 208 | self.journey_email_id = "787y87y87y87y87y87y87" 209 | self.journey_email = JourneyEmail( 210 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, self.journey_email_id) 211 | 212 | 213 | class ApiKeyCampaignTestCase(unittest.TestCase, JourneyEmailTestCase): 214 | """Test when using an API key to authenticate""" 215 | 216 | def setUp(self): 217 | self.journey_email_id = "787y87y87y87y87y87y87" 218 | self.journey_email = JourneyEmail( 219 | {'api_key': '123123123123123123123'}, self.journey_email_id) 220 | -------------------------------------------------------------------------------- /test/test_subscriber.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import unittest 3 | 4 | from createsend.createsend import BadRequest 5 | from createsend.subscriber import Subscriber 6 | 7 | 8 | class SubscriberTestCase: 9 | 10 | def test_get(self): 11 | email = "subscriber@example.com" 12 | self.subscriber.stub_request("subscribers/%s.json?email=%s&includetrackingpreference=False" % 13 | (self.list_id, quote(email)), "subscriber_details.json") 14 | subscriber = self.subscriber.get(self.list_id, email) 15 | self.assertEqual(subscriber.EmailAddress, email) 16 | self.assertEqual(subscriber.Name, "Subscriber One") 17 | self.assertEqual(subscriber.Date, "2010-10-25 10:28:00") 18 | self.assertEqual(subscriber.ListJoinedDate, "2010-10-25 10:28:00") 19 | self.assertEqual(subscriber.State, "Active") 20 | self.assertEqual(len(subscriber.CustomFields), 3) 21 | self.assertEqual(subscriber.CustomFields[0].Key, 'website') 22 | self.assertEqual(subscriber.CustomFields[ 23 | 0].Value, 'https://example.com') 24 | self.assertEqual(subscriber.ReadsEmailWith, "Gmail") 25 | 26 | def test_get_without_arguments(self): 27 | email = "subscriber@example.com" 28 | self.subscriber.stub_request("subscribers/%s.json?email=%s&includetrackingpreference=False" % 29 | (self.list_id, quote(email)), "subscriber_details.json") 30 | subscriber = self.subscriber.get() 31 | self.assertEqual(subscriber.EmailAddress, email) 32 | self.assertEqual(subscriber.Name, "Subscriber One") 33 | self.assertEqual(subscriber.Date, "2010-10-25 10:28:00") 34 | self.assertEqual(subscriber.ListJoinedDate, "2010-10-25 10:28:00") 35 | self.assertEqual(subscriber.State, "Active") 36 | self.assertEqual(len(subscriber.CustomFields), 3) 37 | self.assertEqual(subscriber.CustomFields[0].Key, 'website') 38 | self.assertEqual(subscriber.CustomFields[ 39 | 0].Value, 'https://example.com') 40 | self.assertEqual(subscriber.ReadsEmailWith, "Gmail") 41 | 42 | def test_get_with_tracking_preference_included(self): 43 | email = "subscriber@example.com" 44 | self.subscriber.stub_request("subscribers/%s.json?email=%s&includetrackingpreference=True" % 45 | (self.list_id, quote(email)), "subscriber_details_with_tracking_preference.json") 46 | subscriber = self.subscriber.get(self.list_id, email, include_tracking_preference=True) 47 | self.assertEqual(subscriber.EmailAddress, email) 48 | self.assertEqual(subscriber.Name, "Subscriber One") 49 | self.assertEqual(subscriber.Date, "2010-10-25 10:28:00") 50 | self.assertEqual(subscriber.ListJoinedDate, "2010-10-25 10:28:00") 51 | self.assertEqual(subscriber.State, "Active") 52 | self.assertEqual(len(subscriber.CustomFields), 3) 53 | self.assertEqual(subscriber.CustomFields[0].Key, 'website') 54 | self.assertEqual(subscriber.CustomFields[ 55 | 0].Value, 'https://example.com') 56 | self.assertEqual(subscriber.ReadsEmailWith, "Gmail") 57 | self.assertEqual(subscriber.ConsentToTrack, "Yes") 58 | 59 | def test_add_without_custom_fields(self): 60 | self.subscriber.stub_request( 61 | "subscribers/%s.json" % self.list_id, "add_subscriber.json") 62 | email_address = self.subscriber.add( 63 | self.list_id, "subscriber@example.com", "Subscriber", [], True, "Unchanged") 64 | self.assertEqual(email_address, "subscriber@example.com") 65 | 66 | def test_add_with_custom_fields(self): 67 | self.subscriber.stub_request( 68 | "subscribers/%s.json" % self.list_id, "add_subscriber.json") 69 | custom_fields = [{"Key": 'website', "Value": 'https://example.com/'}] 70 | email_address = self.subscriber.add( 71 | self.list_id, "subscriber@example.com", "Subscriber", custom_fields, True, "No") 72 | self.assertEqual(email_address, "subscriber@example.com") 73 | 74 | def test_add_with_custom_fields_including_multioption(self): 75 | self.subscriber.stub_request( 76 | "subscribers/%s.json" % self.list_id, "add_subscriber.json") 77 | custom_fields = [{"Key": 'multioptionselectone', "Value": 'myoption'}, 78 | {"Key": 'multioptionselectmany', "Value": 'firstoption'}, 79 | {"Key": 'multioptionselectmany', "Value": 'secondoption'}] 80 | email_address = self.subscriber.add( 81 | self.list_id, "subscriber@example.com", "Subscriber", custom_fields, True, "Yes") 82 | self.assertEqual(email_address, "subscriber@example.com") 83 | 84 | def test_update_with_custom_fields(self): 85 | new_email = "new_email_address@example.com" 86 | self.subscriber.stub_request("subscribers/%s.json?email=%s" % 87 | (self.list_id, quote(self.subscriber.email_address)), None) 88 | custom_fields = [{"Key": 'website', "Value": 'https://example.com/'}] 89 | self.subscriber.update(new_email, "Subscriber", custom_fields, True, "Yes") 90 | self.assertEqual(self.subscriber.email_address, new_email) 91 | 92 | def test_update_with_custom_fields_including_clear_option(self): 93 | new_email = "new_email_address@example.com" 94 | self.subscriber.stub_request("subscribers/%s.json?email=%s" % 95 | (self.list_id, quote(self.subscriber.email_address)), None) 96 | custom_fields = [ 97 | {"Key": 'website', "Value": 'https://example.com/', "Clear": True}] 98 | self.subscriber.update(new_email, "Subscriber", custom_fields, True, "No") 99 | self.assertEqual(self.subscriber.email_address, new_email) 100 | 101 | def test_import_subscribers(self): 102 | self.subscriber.stub_request( 103 | "subscribers/%s/import.json" % self.list_id, "import_subscribers.json") 104 | subscribers = [ 105 | {"EmailAddress": "example+1@example.com", "Name": "Example One"}, 106 | {"EmailAddress": "example+2@example.com", "Name": "Example Two"}, 107 | {"EmailAddress": "example+3@example.com", "Name": "Example Three"}, 108 | ] 109 | import_result = self.subscriber.import_subscribers( 110 | self.list_id, subscribers, True) 111 | self.assertEqual(len(import_result.FailureDetails), 0) 112 | self.assertEqual(import_result.TotalUniqueEmailsSubmitted, 3) 113 | self.assertEqual(import_result.TotalExistingSubscribers, 0) 114 | self.assertEqual(import_result.TotalNewSubscribers, 3) 115 | self.assertEqual(len(import_result.DuplicateEmailsInSubmission), 0) 116 | 117 | def test_import_subscribers_start_subscription_autoresponders(self): 118 | self.subscriber.stub_request( 119 | "subscribers/%s/import.json" % self.list_id, "import_subscribers.json") 120 | subscribers = [ 121 | {"EmailAddress": "example+1@example.com", "Name": "Example One"}, 122 | {"EmailAddress": "example+2@example.com", "Name": "Example Two"}, 123 | {"EmailAddress": "example+3@example.com", "Name": "Example Three"}, 124 | ] 125 | import_result = self.subscriber.import_subscribers( 126 | self.list_id, subscribers, True, True) 127 | self.assertEqual(len(import_result.FailureDetails), 0) 128 | self.assertEqual(import_result.TotalUniqueEmailsSubmitted, 3) 129 | self.assertEqual(import_result.TotalExistingSubscribers, 0) 130 | self.assertEqual(import_result.TotalNewSubscribers, 3) 131 | self.assertEqual(len(import_result.DuplicateEmailsInSubmission), 0) 132 | 133 | def test_import_subscribers_with_custom_fields_including_clear_option(self): 134 | self.subscriber.stub_request( 135 | "subscribers/%s/import.json" % self.list_id, "import_subscribers.json") 136 | subscribers = [ 137 | {"EmailAddress": "example+1@example.com", "Name": "Example One", 138 | "CustomFields": [{"Key": "website", "Value": "", "Clear": True}]}, 139 | {"EmailAddress": "example+2@example.com", "Name": "Example Two", 140 | "CustomFields": [{"Key": "website", "Value": "", "Clear": False}]}, 141 | {"EmailAddress": "example+3@example.com", "Name": "Example Three", 142 | "CustomFields": [{"Key": "website", "Value": "", "Clear": False}]}, 143 | ] 144 | import_result = self.subscriber.import_subscribers( 145 | self.list_id, subscribers, True) 146 | self.assertEqual(len(import_result.FailureDetails), 0) 147 | self.assertEqual(import_result.TotalUniqueEmailsSubmitted, 3) 148 | self.assertEqual(import_result.TotalExistingSubscribers, 0) 149 | self.assertEqual(import_result.TotalNewSubscribers, 3) 150 | self.assertEqual(len(import_result.DuplicateEmailsInSubmission), 0) 151 | 152 | def test_import_subscribers_partial_success(self): 153 | # Stub request with 400 Bad Request as the expected response status 154 | self.subscriber.stub_request("subscribers/%s/import.json" % 155 | self.list_id, "import_subscribers_partial_success.json", 400) 156 | subscribers = [ 157 | {"EmailAddress": "example+1@example", "Name": "Example One"}, 158 | {"EmailAddress": "example+2@example.com", "Name": "Example Two"}, 159 | {"EmailAddress": "example+3@example.com", "Name": "Example Three"}, 160 | ] 161 | import_result = self.subscriber.import_subscribers( 162 | self.list_id, subscribers, True) 163 | self.assertEqual(len(import_result.FailureDetails), 1) 164 | self.assertEqual(import_result.FailureDetails[ 165 | 0].EmailAddress, "example+1@example") 166 | self.assertEqual(import_result.FailureDetails[0].Code, 1) 167 | self.assertEqual(import_result.FailureDetails[ 168 | 0].Message, "Invalid Email Address") 169 | self.assertEqual(import_result.TotalUniqueEmailsSubmitted, 3) 170 | self.assertEqual(import_result.TotalExistingSubscribers, 2) 171 | self.assertEqual(import_result.TotalNewSubscribers, 0) 172 | self.assertEqual(len(import_result.DuplicateEmailsInSubmission), 0) 173 | 174 | def test_import_subscribers_complete_failure_because_of_bad_request(self): 175 | # Stub request with 400 Bad Request as the expected response status 176 | self.subscriber.stub_request( 177 | "subscribers/%s/import.json" % self.list_id, "custom_api_error.json", 400) 178 | subscribers = [ 179 | {"EmailAddress": "example+1@example", "Name": "Example One"}, 180 | {"EmailAddress": "example+2@example.com", "Name": "Example Two"}, 181 | {"EmailAddress": "example+3@example.com", "Name": "Example Three"}, 182 | ] 183 | self.assertRaises( 184 | BadRequest, self.subscriber.import_subscribers, self.list_id, subscribers, True) 185 | 186 | def test_unsubscribe(self): 187 | self.subscriber.stub_request( 188 | "subscribers/%s/unsubscribe.json" % self.list_id, None) 189 | self.subscriber.unsubscribe() 190 | 191 | def test_history(self): 192 | self.subscriber.stub_request("subscribers/{}/history.json?email={}".format( 193 | self.list_id, quote(self.subscriber.email_address)), "subscriber_history.json") 194 | history = self.subscriber.history() 195 | self.assertEqual(len(history), 1) 196 | self.assertEqual(history[0].Name, "Campaign One") 197 | self.assertEqual(history[0].Type, "Campaign") 198 | self.assertEqual(history[0].ID, "fc0ce7105baeaf97f47c99be31d02a91") 199 | self.assertEqual(len(history[0].Actions), 6) 200 | self.assertEqual(history[0].Actions[0].Event, "Open") 201 | self.assertEqual(history[0].Actions[0].Date, "2010-10-12 13:18:00") 202 | self.assertEqual(history[0].Actions[0].IPAddress, "192.168.126.87") 203 | self.assertEqual(history[0].Actions[0].Detail, "") 204 | 205 | def test_delete(self): 206 | self.subscriber.stub_request("subscribers/%s.json?email=%s" % 207 | (self.list_id, quote(self.subscriber.email_address)), None) 208 | self.subscriber.delete() 209 | 210 | 211 | class OAuthSubscriberTestCase(unittest.TestCase, SubscriberTestCase): 212 | """Test when using OAuth to authenticate""" 213 | 214 | def setUp(self): 215 | self.list_id = "d98h2938d9283d982u3d98u88" 216 | self.subscriber = Subscriber( 217 | {"access_token": "ASP95S4aR+9KsgfHB0dapTYxNA==", 218 | "refresh_token": "5S4aASP9R+9KsgfHB0dapTYxNA=="}, 219 | self.list_id, "subscriber@example.com") 220 | 221 | 222 | class ApiKeySubscriberTestCase(unittest.TestCase, SubscriberTestCase): 223 | """Test when using an API key to authenticate""" 224 | 225 | def setUp(self): 226 | self.list_id = "d98h2938d9283d982u3d98u88" 227 | self.subscriber = Subscriber( 228 | {'api_key': '123123123123123123123'}, 229 | self.list_id, "subscriber@example.com") 230 | --------------------------------------------------------------------------------