Skip to content

Commit

Permalink
EveryAction: Add email endpoint methods to retrieve email stats from …
Browse files Browse the repository at this point in the history
…TargetedEmail (move-coop#1003)

* added head and tail methods to parsons table

* adding tests and docs to head and tail

* added email endpoint with email stats transformation

* Add test cases for getting email and email stats

* Revert "added head and tail methods to parsons table"

This reverts commit 2a4290d.

* Revert "adding tests and docs to head and tail"

This reverts commit a885d11.

* fixed tests

* feat: Add ascending/descending sorting option to get_emails method

The `get_emails` method now accepts an optional `ascending` parameter to specify the sorting order for the `dateModified` field. By default, the emails are sorted in ascending order. This change allows users to retrieve emails in either ascending or descending order based on their preference.

* removed print

---------

Co-authored-by: matthewkrausse <[email protected]>
Co-authored-by: mkrausse-ggtx <[email protected]>
Co-authored-by: Shauna <[email protected]>
  • Loading branch information
4 people authored May 14, 2024
1 parent e491cef commit 66f288e
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
150 changes: 150 additions & 0 deletions parsons/ngpvan/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from parsons.etl.table import Table
import logging

logger = logging.getLogger(__name__)


class Email(object):
"""
Instantiate the Email class.
You can find the docs for the NGP VAN Email API here:
https://docs.ngpvan.com/reference/email-overview
"""

def __init__(self, van_connection):

self.connection = van_connection

def get_emails(self, ascending: bool = True) -> Table:
"""
Get emails.
`Args:`
ascending : Bool
sorts results in ascending or descending order
for the dateModified field. Defaults to True (ascending).
`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""
if ascending:
params = {
"$orderby": "dateModified asc",
}
if not ascending:
params = {
"$orderby": "dateModified desc",
}

tbl = Table(self.connection.get_request("email/messages", params=params))
logger.debug(f"Found {tbl.num_rows} emails.")
return tbl

def get_email(self, email_id: int, expand: bool = True) -> Table:
"""
Get an email.
Note that it takes some time for the system to aggregate opens and click-throughs,
so data can be delayed up to 15 minutes.
`Args:`
email_id : int
The email id.
expand : bool
Optional; expands the email message to include the email content and
statistics. Defaults to True.
`Returns:`
dict
"""

params = {
"$expand": (
"emailMessageContent, EmailMessageContentDistributions"
if expand
else None
),
}

r = self.connection.get_request(f"email/message/{email_id}", params=params)
logger.debug(f"Found email {email_id}.")
return r

def get_email_stats(self) -> Table:
"""
Get stats for all emails, aggregating any A/B tests.
`Args:`
emails : list
A list of email message details.
`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

email_list = []

final_email_list = []

emails = self.get_emails()

foreign_message_ids = [email["foreignMessageId"] for email in emails]

for fmid in foreign_message_ids:
email = self.get_email(fmid)
email_list.append(email)

for email in email_list:
d = {}
d["name"] = email["name"]
d["createdBy"] = email["createdBy"]
d["dateCreated"] = email["dateCreated"]
d["dateModified"] = email["dateModified"]
d["dateScheduled"] = email["dateScheduled"]
d["foreignMessageId"] = email["foreignMessageId"]
d["recipientCount"] = 0
d["bounceCount"] = 0
d["contributionCount"] = 0
d["contributionTotal"] = 0
d["formSubmissionCount"] = 0
d["linksClickedCount"] = 0
d["machineOpenCount"] = 0
d["openCount"] = 0
d["unsubscribeCount"] = 0
try:
for i in email["emailMessageContent"]:
d["recipientCount"] += i["emailMessageContentDistributions"][
"recipientCount"
]
d["bounceCount"] += i["emailMessageContentDistributions"][
"bounceCount"
]
d["contributionCount"] += i["emailMessageContentDistributions"][
"contributionCount"
]
d["contributionTotal"] += i["emailMessageContentDistributions"][
"contributionTotal"
]
d["formSubmissionCount"] += i["emailMessageContentDistributions"][
"formSubmissionCount"
]
d["linksClickedCount"] += i["emailMessageContentDistributions"][
"linksClickedCount"
]
d["machineOpenCount"] += i["emailMessageContentDistributions"][
"machineOpenCount"
]
d["openCount"] += i["emailMessageContentDistributions"]["openCount"]
d["unsubscribeCount"] += i["emailMessageContentDistributions"][
"unsubscribeCount"
]
except TypeError as e:
logger.info(str(e))
pass

final_email_list.append(d)

return Table(final_email_list)
2 changes: 2 additions & 0 deletions parsons/ngpvan/van.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from parsons.ngpvan.events import Events
from parsons.ngpvan.email import Email
from parsons.ngpvan.van_connector import VANConnector
from parsons.ngpvan.people import People
from parsons.ngpvan.saved_lists import SavedLists, Folders, ExportJobs
Expand All @@ -24,6 +25,7 @@
class VAN(
People,
Events,
Email,
SavedLists,
PrintedLists,
Folders,
Expand Down
111 changes: 111 additions & 0 deletions test/test_van/test_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import unittest
import os
import requests_mock
from parsons import VAN, Table


def assert_matching_tables(table1, table2, ignore_headers=False):
if ignore_headers:
data1 = table1.data
data2 = table2.data
else:
data1 = table1
data2 = table2

if isinstance(data1, Table) and isinstance(data2, Table):
assert data1.num_rows == data2.num_rows

for r1, r2 in zip(data1, data2):
# Cast both rows to lists, in case they are different types of collections. Must call
# .items() on dicts to compare content of collections
if isinstance(r1, dict):
r1 = r1.items()
if isinstance(r2, dict):
r2 = r2.items()

assert list(r1) == list(r2)


os.environ["VAN_API_KEY"] = "SOME_KEY"

mock_response = [
{
"foreignMessageId": "oK2ahdAcEe6F-QAiSCI3lA2",
"name": "Test Email",
"createdBy": "Joe Biden",
"dateCreated": "2024-02-20T13:20:00Z",
"dateScheduled": "2024-02-20T15:55:00Z",
"campaignID": 0,
"dateModified": "2024-02-20T15:54:36.27Z",
"emailMessageContent": None,
},
{
"foreignMessageId": "rjzc2szzEe6F-QAiSCI3lA2",
"name": "Test Email 2",
"createdBy": "Joe Biden",
"dateCreated": "2024-02-16T12:49:00Z",
"dateScheduled": "2024-02-16T13:29:00Z",
"campaignID": 0,
"dateModified": "2024-02-16T13:29:16.453Z",
"emailMessageContent": None,
},
{
"foreignMessageId": "_E1AfcnkEe6F-QAiSCI3lA2",
"name": "Test Email 3",
"createdBy": "Joe Biden",
"dateCreated": "2024-02-12T15:26:00Z",
"dateScheduled": "2024-02-13T11:22:00Z",
"campaignID": 0,
"dateModified": "2024-02-13T11:22:28.273Z",
"emailMessageContent": None,
},
{
"foreignMessageId": "6GTLBsUwEe62YAAiSCIxlw2",
"name": "Test Email 4",
"createdBy": "Joe Biden",
"dateCreated": "2024-02-06T15:47:00Z",
"dateScheduled": "2024-02-07T10:32:00Z",
"campaignID": 0,
"dateModified": "2024-02-07T10:31:55.16Z",
"emailMessageContent": None,
},
{
"foreignMessageId": "mgTdmcEiEe62YAAiSCIxlw2",
"name": "Test Email 5",
"createdBy": "Joe Biden",
"dateCreated": "2024-02-01T11:55:00Z",
"dateScheduled": "2024-02-01T16:08:00Z",
"campaignID": 0,
"dateModified": "2024-02-01T16:08:10.737Z",
"emailMessageContent": None,
},
]


class TestEmail(unittest.TestCase):
def setUp(self):
self.van = VAN(os.environ["VAN_API_KEY"], db="MyVoters", raise_for_status=False)

@requests_mock.Mocker()
def test_get_email_messages(self, m):
m.get(
self.van.connection.uri + "email/messages",
json=mock_response,
status_code=200,
)

response = self.van.get_emails()

assert_matching_tables(response, mock_response)

@requests_mock.Mocker()
def test_get_email_message(self, m):
m.get(
self.van.connection.uri + "email/message/1",
json=mock_response[0],
status_code=200,
)

response = self.van.get_email(1)

assert_matching_tables(response, mock_response[0])

0 comments on commit 66f288e

Please sign in to comment.