diff --git a/examples/helpers/campaigns/campaigns.py b/examples/helpers/campaigns/campaigns.py new file mode 100644 index 000000000..41a408c86 --- /dev/null +++ b/examples/helpers/campaigns/campaigns.py @@ -0,0 +1,76 @@ +import json +import os +from sendgrid.helpers.campaigns import * +from sendgrid import * + +# NOTE: you will need move this file to the root directory of this project to execute properly. + +# Assumes you set your environment variable: +# https://github.com/sendgrid/sendgrid-python/blob/master/TROUBLESHOOTING.md#environment-variables-and-your-sendgrid-api-key +SG = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + + +def pprint_json(json_raw): + print(json.dumps(json.loads(json_raw), indent=4, sort_keys=True)) + + +def print_campaigns(): + response = sg.client.categories.campaigns.get(query_params=stats_params) + print(response.status_code) + print(response.headers) + pprint_json(response.body) + + +def get_campaigns(): + """Get Campaign objects""" + campaigns = [] + response = SG.client.campaigns.get() + for camp in json.loads(response.body.decode())["result"]: + campaigns.append(Campaign(**camp)) + return campaigns + + +def create_campaign(call_func=False): + camp_settings = { + "title": "Test Campaign 1", + "categories": ["test", "example"], + "html_content": "New Edition!

New edition is now live!

", + } + camp = Campaign(**camp_settings) + + if call_func: + campaign_build(SG, camp) + else: + SG.client.campaigns.post(request_body=camp.get()) + + +def schedule_campaign(): + """Schedule the first campaign retrieved""" + # campaigns = [] + + campaigns = Campaigns(offset=0, limit=2) + response = SG.client.campaigns.get(query_params=campaigns.get()) + for camp in json.loads(response.body.decode())["result"]: + print(schedule.get()) + print(camp["title"]) + c_response = getattr(SG.client.campaigns, str(camp["id"])).schedules.get() + pprint_json(c_response.body.decode()) + + schedule = Schedule(year=2018, month=12, day=1, hour=8, minute=23) + c_response = getattr(SG.client.campaigns, str(camp["id"])).schedules.post( + request_body=schedule.get() + ) + pprint_json(c_response.body.decode()) + + c_response = getattr(SG.client.campaigns, str(camp["id"])).schedules.get() + pprint_json(c_response.body.decode()) + break + + +def main(): + get_campaigns() + create_campaign() + + +if __name__ == "__main__": + main() diff --git a/sendgrid/helpers/campaigns/__init__.py b/sendgrid/helpers/campaigns/__init__.py new file mode 100644 index 000000000..b1df7a8be --- /dev/null +++ b/sendgrid/helpers/campaigns/__init__.py @@ -0,0 +1,6 @@ +from .campaign import Campaign +from .campaigns import Campaigns +from .schedule import Schedule +from .campaign_build import campaign_build +from .campaign_build_send import campaign_build_send +from .campaign_build_scheduled import campaign_build_scheduled diff --git a/sendgrid/helpers/campaigns/campaign.py b/sendgrid/helpers/campaigns/campaign.py new file mode 100644 index 000000000..a8511ae4a --- /dev/null +++ b/sendgrid/helpers/campaigns/campaign.py @@ -0,0 +1,230 @@ +class Campaign(object): + """Campaign class object for campaign API queries + + Required parameter: + title: str + + Optional parameters: + categories: list(str) + custom_unsubscribe_url: str + editor: str ['code' | 'design'] + html_content: str + ip_pool: str + list_ids: list(int) + plain_content: str + segment_ids: list(int) + sender_id: int + subject: str + suppression_group_id: int + + :param kwargs: List of inputs + """ + def __init__(self, **kwargs): + self._categories = [] + self._custom_unsubscribe_url = None + self._editor = None + self._html_content = None + self._id = None + self._ip_pool = None + self._list_ids = [] + self._plain_content = None + self._segment_ids = [] + self._sender_id = None + self._status = "" + self._subject = None + self._suppression_group_id = None + self._title = "" + + for key, val in kwargs.items(): + if hasattr(self, key): + setattr(self, key, val) + + def copy(self, new_title): + """Creates a copy of the campaign + + :param new_title: Title of new campaign + :type new_title: str + :return: Campaign + """ + new_camp = Campaign(**self.get()) + new_camp.title = new_title + return new_camp + + def get(self): + """Returns dict suitable for queries""" + body = {"title": self.title} + if self.categories: + body["categories"] = [str(i) for i in self.categories] + if self.custom_unsubscribe_url is not None: + body["custom_unsubscribe_url"] = self.custom_unsubscribe_url + if self.editor is not None: + body["editor"] = self.editor + if self.html_content is not None: + body["html_content"] = self.html_content + if self.ip_pool is not None: + body["ip_pool"] = self.ip_pool + if self.list_ids and all(isinstance(i, int) for i in self.list_ids): + body["list_ids"] = self.list_ids + if self.plain_content: + body["plain_content"] = self.plain_content + if self.segment_ids\ + and all(isinstance(i, int) for i in self.segment_ids): + body["segment_ids"] = self.segment_ids + if self.sender_id is not None: + body["sender_id"] = self.sender_id + if self.subject is not None: + body["subject"] = self.subject + if self.suppression_group_id is not None: + body["suppression_group_id"] = self.suppression_group_id + return body + + def get_patch(self): + return { + "title": self.title, + "subject": self.subject, + "categories": self.categories, + "html_content": self.html_content, + "plain_content": self.plain_content + } + + def patch(self, **kwargs): + """Updates the campaign with + + Optional inputs: + title: str + subject: str + categories: list(str) + html_content: str + plain_content: str + + :param kwargs: Dictionary of optional inputs + :type kwargs: dict + :return: Updated get() dict + """ + if "title" in kwargs: + self.title = kwargs["title"] + if "subject" in kwargs: + self.subject = kwargs["subject"] + if "categories" in kwargs: + self.categories = kwargs["categories"] + if "html_content" in kwargs: + self.html_content = kwargs["html_content"] + if "plain_content" in kwargs: + self.plain_content = kwargs["plain_content"] + return self.get_patch() + + @property + def categories(self): + return self._categories + + @categories.setter + def categories(self, value): + self._categories = value + + @property + def custom_unsubscribe_url(self): + return self._custom_unsubscribe_url + + @custom_unsubscribe_url.setter + def custom_unsubscribe_url(self, value): + self._custom_unsubscribe_url = value + + @property + def editor(self): + return self._editor + + @editor.setter + def editor(self, value): + value = str(value).lower() + if value in ["code", "design"]: + self._editor = value + + @property + def html_content(self): + return self._html_content + + @html_content.setter + def html_content(self, value): + self._html_content = value + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + self._id = value + + @property + def ip_pool(self): + return self._ip_pool + + @ip_pool.setter + def ip_pool(self, value): + self._ip_pool = value + + @property + def list_ids(self): + return self._list_ids + + @list_ids.setter + def list_ids(self, value): + self._list_ids = value + + @property + def plain_content(self): + return self._plain_content + + @plain_content.setter + def plain_content(self, value): + self._plain_content = value + + @property + def segment_ids(self): + return self._segment_ids + + @segment_ids.setter + def segment_ids(self, value): + self._segment_ids = value + + @property + def sender_id(self): + return self._sender_id + + @sender_id.setter + def sender_id(self, value): + self._sender_id = value + + @property + def status(self): + return self._status + + @status.setter + def status(self, value): + self._status = value + + @property + def subject(self): + return self._subject + + @subject.setter + def subject(self, value): + if isinstance(value, str) or isinstance(value, type(None)): + self._subject = value + + @property + def suppression_group_id(self): + return self._suppression_group_id + + @suppression_group_id.setter + def suppression_group_id(self, value): + self._suppression_group_id = value + + @property + def title(self): + return self._title + + @title.setter + def title(self, value): + if value: + self._title = str(value) diff --git a/sendgrid/helpers/campaigns/campaign_build.py b/sendgrid/helpers/campaigns/campaign_build.py new file mode 100644 index 000000000..8f7b08fb6 --- /dev/null +++ b/sendgrid/helpers/campaigns/campaign_build.py @@ -0,0 +1,24 @@ +import json + + +def campaign_build(api_client, campaign, notify=None): + """Build a Campaign via the SendGrid API + + :param api_client: SendGrid API Client + :type api_client: sendgrid.SendGridAPIClient + :param campaign: Campaign object to build + :type campaign: sendgrid.helpers.campaigns.Campaign + :param notify: Mail objects to send on completion + :type notify: list,sendgrid.helpers.mail.mail.Mail + :return: Campaign.id for newly created Campaign + :rtype: int + """ + response = api_client.client.campaigns.post(request_body=campaign.get()) + camp_id = json.loads(response.body.decode())["id"] + if notify is not None: + if isinstance(notify, list): + for n in notify: + api_client.client.mail.send.post(request_body=n.get()) + else: + api_client.client.mail.send.post(request_body=notify.get()) + return camp_id diff --git a/sendgrid/helpers/campaigns/campaign_build_scheduled.py b/sendgrid/helpers/campaigns/campaign_build_scheduled.py new file mode 100644 index 000000000..86ecb71bd --- /dev/null +++ b/sendgrid/helpers/campaigns/campaign_build_scheduled.py @@ -0,0 +1,30 @@ +from .campaign_build import campaign_build + + +def campaign_build_scheduled(api_client, campaign, schedule, notify=None): + """Builds a campaign and schedules for release + + See sendgrid.helpers.campaigns.campaign.get_schedule for kwargs + + :param api_client: SendGrid API Client + :type api_client: sendgrid.SendGridAPIClient + :param campaign: Campaign object to build + :type campaign: sendgrid.helpers.campaigns.Campaign + :param notify: Mail objects to send on completion + :type notify: list,sendgrid.helpers.mail.mail.Mail + :param schedule: Schedule object to get schedule body + :type schedule: sendgrid.helpers.campaigns.schedule.Schedule + :return: ID of created campaign + :rtype: int + """ + c_id = campaign_build(api_client, campaign) + getattr(api_client.client.campaigns, str(c_id)).schedules.post( + request_body=schedule.get() + ) + if notify is not None: + if isinstance(notify, list): + for n in notify: + api_client.client.mail.send.post(request_body=n.get()) + else: + api_client.client.mail.send.post(request_body=notify.get()) + return c_id diff --git a/sendgrid/helpers/campaigns/campaign_build_send.py b/sendgrid/helpers/campaigns/campaign_build_send.py new file mode 100644 index 000000000..3a86764c0 --- /dev/null +++ b/sendgrid/helpers/campaigns/campaign_build_send.py @@ -0,0 +1,24 @@ +from .campaign_build import campaign_build + + +def campaign_build_send(api_client, campaign, notify=None): + """Builds a campaign and sends immediately + + :param api_client: SendGrid API Client + :type api_client: sendgrid.SendGridAPIClient + :param campaign: Campaign object to build + :type campaign: sendgrid.helpers.campaigns.Campaign + :param notify: Mail objects to send on completion + :type notify: list,sendgrid.helpers.mail.mail.Mail + :return: ID of created campaign + :rtype: int + """ + c_id = campaign_build(api_client, campaign) + getattr(api_client.client.campaigns, str(c_id)).schedules.now.post() + if notify is not None: + if isinstance(notify, list): + for n in notify: + api_client.client.mail.send.post(request_body=n.get()) + else: + api_client.client.mail.send.post(request_body=notify.get()) + return c_id diff --git a/sendgrid/helpers/campaigns/campaigns.py b/sendgrid/helpers/campaigns/campaigns.py new file mode 100644 index 000000000..0056473a1 --- /dev/null +++ b/sendgrid/helpers/campaigns/campaigns.py @@ -0,0 +1,46 @@ +class Campaigns(object): + def __init__(self, limit=10, offset=0): + self._limit = limit + self._offset = offset + + def get(self, limit=None, offset=None): + """Get all available campaigns + + Stores all Campaigns in 'campaigns' property and retrieves/populates + campaign IDs, which are stored in 'ids' property. + + :param limit: Number of results to receive + :type limit: int + :param offset: Index of first campaign to receive + :type offset: int + :return: List of Campaigns + :rtype: list(Campaign) + """ + params = {} + if limit is None: + limit = self._limit + if isinstance(limit, int): + params["limit"] = self._limit + + if offset is None: + offset = self._offset + if isinstance(offset, int): + params["offset"] = offset + + return params + + @property + def limit(self): + return self._limit + + @limit.setter + def limit(self, value): + self._limit = int(value) + + @property + def offset(self): + return self._offset + + @offset.setter + def offset(self, value): + self._offset = int(value) diff --git a/sendgrid/helpers/campaigns/schedule.py b/sendgrid/helpers/campaigns/schedule.py new file mode 100644 index 000000000..084299a2b --- /dev/null +++ b/sendgrid/helpers/campaigns/schedule.py @@ -0,0 +1,131 @@ +from datetime import datetime, timedelta + + +class Schedule(object): + """Get schedule-accepted Unix time stamp + + Python datetime.datetime or Unix timestamp are both accepted. + If send_at is None, the value will be created with other inputs + + :param send_at: Unix timestamp + :type send_at: int, datetime.datetime + :param year: Scheduled year (current+) + :type year: int + :param month: Scheduled year [1..12] + :type month: int + :param day: Scheduled year [1..31] + :type day: int + :param hour: Scheduled hour [0..23] + :type hour: int + :param minute: Scheduled minute [0..59] + :return: `send_at`=Unix timestamp dict + :rtype: dict + """ + def __init__(self, send_at=None, year=None, month=None, + day=None, hour=None, minute=None): + + def_dt = datetime.now() + timedelta(days=1) + self._minute = 0 + self._hour = 0 + self._day = def_dt.day + self._month = def_dt.month + self._year = def_dt.year + + if send_at is not None: + if isinstance(send_at, datetime): + self.timestamp = self.convert_datetime(send_at) + else: + self.timestamp = int(send_at) + self.parse_timestamp() + + if send_at is None: + if year is not None: + self.year = year + if month is not None: + self.month = month + if day is not None: + self.day = day + if hour is not None: + self.hour = hour + if minute is not None: + self.minute = minute + self._timestamp = self.convert_datetime( + datetime(self.year, self.month, self.day, self.hour, self.minute) + ) + + @staticmethod + def convert_datetime(dt): + """Converts a datetime to Unix time stamp + + :param dt: Datetime to parse + :type dt: datetime.datetime + :return: Unix timestamp + :rtype: int + """ + t_delta = dt - datetime(1970, 1, 1) + return t_delta.seconds + t_delta.days * 86400 + + def get(self): + return {"send_at": self._timestamp} + + def parse_timestamp(self): + """Parses the Schedule object's timestamp into year, month, etc. + + :return: Parsed datetime object + :rtype: datetime.datetime + """ + dt = datetime(1970, 1, 1) + timedelta(seconds=self._timestamp) + self.year = dt.year + self.month = dt.month + self.day = dt.day + self.hour = dt.hour + self.minute = dt.minute + return dt + + @property + def day(self): + return self._day + + @day.setter + def day(self, value): + self._day = value + + @property + def hour(self): + return self._hour + + @hour.setter + def hour(self, value): + self._hour = value + + @property + def minute(self): + return self._minute + + @minute.setter + def minute(self, value): + self._minute = value + + @property + def month(self): + return self._month + + @month.setter + def month(self, value): + self._month = value + + @property + def timestamp(self): + return self._timestamp + + @timestamp.setter + def timestamp(self, value): + self._timestamp = int(value) + + @property + def year(self): + return self._year + + @year.setter + def year(self, value): + self._year = value diff --git a/test/test_campaigns.py b/test/test_campaigns.py new file mode 100644 index 000000000..e0605354d --- /dev/null +++ b/test/test_campaigns.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +from sendgrid.helpers.campaigns import * +try: + import unittest2 as unittest +except ImportError: + import unittest + + +class TestCampaignObject(unittest.TestCase): + + def test_set_title(self): + c_name = "Test Campaign" + camp = Campaign(title=c_name) + self.assertEqual(camp.title, "Test Campaign") + + def test_campaign_get(self): + data = { + "categories": [ + "summer line" + ], + "custom_unsubscribe_url": "test.url", + "editor": "code", + "html_content": "

Check out our summer line!

", + "ip_pool": "test_ips", + "list_ids": [2, 3], + "plain_content": "Check out our summer line!", + "segment_ids": [2, 3], + "sender_id": 4324, + "subject": "New Products for Summer!", + "title": "May Newsletter", + "suppression_group_id": 5123 + } + camp = Campaign(**data) + self.assertDictEqual(camp.get(), data) + + def test_campaign_patch(self): + data = { + "categories": [ + "summer line" + ], + "html_content": "

Check out our summer line!

", + "plain_content": "Check out our summer line!", + "subject": "New Products for Summer!", + "title": "May Newsletter" + } + camp = Campaign(**data) + new_data = { + "categories": [ + "fall line" + ], + "html_content": "

Check out our summer line!

", + "plain_content": "Check out our summer line!", + "subject": "New Products for Fall!", + "title": "August Newsletter" + } + self.assertDictEqual(camp.patch(**new_data), new_data) + + def test_campaign_copy(self): + data = { + "plain_content": "Check out our summer line!", + "subject": "New Products for Summer!", + "title": "May Newsletter" + } + camp = Campaign(**data) + camp2 = camp.copy("June Newsletter") + self.assertEqual(camp.subject, camp2.subject) + self.assertEqual(camp.plain_content, camp2.plain_content) + self.assertNotEqual(camp.title, camp2.title) + + +class TestCampaignsObject(unittest.TestCase): + + def test_campaigns_get(self): + lim = 5 + offset = 20 + camps = Campaigns(offset=offset, limit=lim) + self.assertEqual(camps.get(), {"offset": offset, "limit": lim}) + + def test_campaigns_props(self): + lim = 5 + offset = 20 + camps = Campaigns(offset=offset, limit=lim) + self.assertEqual(camps.offset, offset) + self.assertEqual(camps.limit, lim) + + +class TestScheduleObject(unittest.TestCase): + + def test_components_to_timestamp(self): + schedule = Schedule(year=2018, month=12, day=1, hour=8, minute=23) + t_delta = datetime(2018, 12, 1, 8, 23) - datetime(1970, 1, 1) + unix_timestamp = t_delta.seconds + t_delta.days * 86400 + self.assertEqual(schedule.timestamp, unix_timestamp) + self.assertDictEqual(schedule.get(), {"send_at": unix_timestamp}) + + def test_timestamp_to_dt(self): + td = datetime(2018, 12, 1, 8, 23) - datetime(1970, 1, 1) + timestamp = td.seconds + td.days * 86400 + schedule = Schedule(send_at=timestamp) + self.assertEqual(schedule.year, 2018) + self.assertEqual(schedule.month, 12) + self.assertEqual(schedule.day, 1) + self.assertEqual(schedule.hour, 8) + self.assertEqual(schedule.minute, 23) + + def test_dt_to_timestamp(self): + schedule = Schedule(datetime(2018, 12, 1, 8, 23)) + t_delta = datetime(2018, 12, 1, 8, 23) - datetime(1970, 1, 1) + unix_timestamp = t_delta.seconds + t_delta.days * 86400 + self.assertEqual(schedule.timestamp, unix_timestamp) + + +if __name__ == "__main__": + unittest.main() diff --git a/use_cases/campaign_helpers.md b/use_cases/campaign_helpers.md new file mode 100644 index 000000000..fced8b236 --- /dev/null +++ b/use_cases/campaign_helpers.md @@ -0,0 +1,39 @@ +# Campaign Helper Usage + +Campaigns can be created using helper classes, then used to generate requests to the SendGrid API. + +```python +import os +from sendgrid import * +from sendgrind.helpers.campaigns import * + +SG = SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY')) + +data = { + "categories": [ + "summer line" + ], + "html_content": "

Check out our summer line!

", + "plain_content": "Check out our summer line!", + "subject": "New Products for Summer!", + "title": "May Newsletter" +} + +camp = Campaign(**data) + +# Use the build helper +campaign_id = campaign_build(SG, camp) + +# OR use the client +response = SG.client.campaigns.post(request_body=camp.get()) +print(response.status_code) +print(response.body) +print(response.headers) + + +# Schedule the campaign +schedule = Schedule(year=2018, month=12, day=1, hour=11, minute=59) +response = getattr(SG.client.campaigns, campaign_id).schedules.post( + request_body=schedule.get() +) +``` \ No newline at end of file