Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kasiah/empower connector #1191

Merged
merged 13 commits into from
Nov 21, 2024
49 changes: 49 additions & 0 deletions docs/empower.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Empower
=======

********
Overview
********

The Empower class allows you to interact with the Empower API. Documentation for the Empower API can be found
in their `GitHub <https://github.com/getempower/api-documentation/blob/master/README.md>`_ repo.

The Empower API only has a single endpoint to access all account data. As such, it has a very high overhead. This
connector employs caching in order to allow the user to specify the tables to extract without additional API calls.
You can disable caching as an argument when instantiating the class.

.. note::
shaunagm marked this conversation as resolved.
Show resolved Hide resolved
To authenticate, request a secret token from Empower.

==========
Quickstart
==========

KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
To instantiate the Empower class, you can either store your ``EMPOWER_API_KEY`` as an environment
variables or pass it in as an argument:

.. code-block:: python

from parsons import Empower

# First approach: Use API key environment variables

# In bash, set your environment variables like so:
# export EMPOWER_API_KEY='MY_API_KEY'
empower = Empower()

# Second approach: Pass API keys as arguments
empower = Empower(api_key='MY_API_KEY')

You can then request tables in the following manner:

.. code-block:: python

tbl = empower.get_profiles()

***
API
***

.. autoclass :: parsons.Empower
:inherited-members:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ Indices and tables
crowdtangle
databases
donorbox
empower
facebook_ads
formstack
freshdesk
Expand Down
1 change: 1 addition & 0 deletions parsons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
("parsons.turbovote.turbovote", "TurboVote"),
("parsons.twilio.twilio", "Twilio"),
("parsons.zoom.zoom", "Zoom"),
("parsons.empower.empower", "Empower"),
):
try:
globals()[connector_name] = getattr(importlib.import_module(module_path), connector_name)
Expand Down
3 changes: 3 additions & 0 deletions parsons/empower/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from parsons.empower.empower import Empower

__all__ = ["Empower"]
258 changes: 258 additions & 0 deletions parsons/empower/empower.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
from parsons.utilities.api_connector import APIConnector
from parsons.utilities import check_env
from parsons.utilities.datetime import convert_unix_to_readable
from parsons.etl import Table
import logging

logger = logging.getLogger(__name__)

EMPOWER_API_ENDPOINT = "https://api.getempower.com/v1/export"


class Empower(object):
"""
Instantiate class.

`Args:`
api_key: str
The Empower provided API key.The Empower provided Client UUID. Not
required if ``EMPOWER_API_KEY`` env variable set.
empower_uri: str
The URI to access the Empower API. The default is currently set to
https://api.getempower.com/v1/export. You can set an ``EMPOWER_URI`` env
variable or use this URI parameter if a different endpoint is necessary.
cache: boolean
The Empower API returns all account data after each call. Setting cache
to ``True`` stores the blob and then extracts Parsons tables for each method.
Setting cache to ``False`` will download all account data for each method call.
"""

def __init__(self, api_key=None, empower_uri=None, cache=True):
self.api_key = check_env.check("EMPOWER_API_KEY", api_key)
self.empower_uri = (
check_env.check("EMPOWER_URI", empower_uri, optional=True) or EMPOWER_API_ENDPOINT
)
self.headers = {"accept": "application/json", "secret-token": self.api_key}
self.client = APIConnector(
self.empower_uri,
headers=self.headers,
)
self.data = None
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
self.data = self._get_data(cache)

def _get_data(self, cache):
"""
Gets fresh data from Empower API based on cache setting.
"""
if not cache or self.data is None:
r = self.client.get_request(self.empower_uri)
logger.info("Empower data downloaded.")
return r

else:
return self.data

def _empty_obj(self, obj_name):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a little weird to pull this out into its own method since it's only used once? But if we think it's likely we'll want re-use the helper method again might as well keep it separated out.

"""
Determine if a dict object is empty.
"""

if len(self.data[obj_name]) == 0:
return True
else:
return False

def get_profiles(self):
"""
Get Empower profiles.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["profiles"])
for col in ["createdMts", "lastUsedEmpowerMts", "updatedMts"]:
tbl.convert_column(col, lambda x: convert_unix_to_readable(x))
tbl.remove_column("activeCtaIds") # Get as a method via get_profiles_active_ctas
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
return tbl

def get_profiles_active_ctas(self):
"""
Get active ctas assigned to Empower profiles.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["profiles"]).long_table("eid", "activeCtaIds")
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
return tbl

def get_regions(self):
"""
Get Empower regions.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["regions"])
tbl.convert_column("inviteCodeCreatedMts", lambda x: convert_unix_to_readable(x))
return tbl

def get_cta_results(self):
"""
Get Empower call to action results.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

# unpacks answerIdsByPromptId into standalone rows
tbl = Table(self.data["ctaResults"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's enough data restructuring happening in this method that I would appreciate some code comments to help readers understand what's happening (ie so for instance one doesn't have to go read the docstring for unpack_nested_columns_as_rows to know the precise behavior - could just say # unpacks answerIdsByPromptId into standalone rows)

tbl.convert_column("contactedMts", lambda x: convert_unix_to_readable(x))
tbl = tbl.unpack_nested_columns_as_rows(
"answerIdsByPromptId", key="profileEid", expand_original=True
)
tbl.unpack_list("answerIdsByPromptId_value", replace=True)
col_list = [v for v in tbl.columns if v.find("value") != -1]
tbl.coalesce_columns("answer_id", col_list, remove_source_columns=True)
tbl.remove_column("uid")
tbl.remove_column("answers") # Per docs, this is deprecated.
return tbl

def _split_ctas(self):
"""
Internal method to split CTA objects into tables.
"""

ctas = Table(self.data["ctas"])
for col in [
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
"createdMts",
"scheduledLaunchTimeMts",
"updatedMts",
"activeUntilMts",
]:
ctas.convert_column(col, lambda x: convert_unix_to_readable(x))
# Get following data as their own tables via their own methods
ctas.remove_column("regionIds") # get_cta_regions()
ctas.remove_column("shareables") # get_cta_shareables()
ctas.remove_column("prioritizations") # get_cta_prioritizations()
ctas.remove_column("questions") # This column has been deprecated.

cta_prompts = ctas.long_table("id", "prompts", prepend=False, retain_original=False)
cta_prompts.remove_column("ctaId")

cta_prompt_answers = cta_prompts.long_table("id", "answers", prepend=False)

return {
"ctas": ctas,
"cta_prompts": cta_prompts,
"cta_prompt_answers": cta_prompt_answers,
}

def get_ctas(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enough data restructuring is happening in these methods (or the private/utility methods they're calling) that it would be useful for each of the dcostrings for the public methods to describe the format of the data being returned.

"""
Get Empower calls to action.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

return self._split_ctas()["ctas"]

def get_cta_prompts(self):
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
"""
Get Empower calls to action prompts.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

return self._split_ctas()["cta_prompts"]

def get_cta_prompt_answers(self):
KasiaHinkson marked this conversation as resolved.
Show resolved Hide resolved
"""
Get Empower calls to action prompt answers.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

return self._split_ctas()["cta_prompt_answers"]

def get_cta_regions(self):
"""
Get a list of regions that each call to active is active in.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["ctas"]).long_table("id", "regionIds")
return tbl

def get_cta_shareables(self):
"""
Get a list of shareables associated with calls to action.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["ctas"]).long_table("id", "shareables")
return tbl

def get_cta_prioritizations(self):
"""
Get a list prioritizations associated with calls to action.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table(self.data["ctas"]).long_table("id", "prioritizations")
return tbl

def get_outreach_entries(self):
"""
Get outreach entries.

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""
if self._empty_obj("outreachEntries"):
logger.info("No Outreach Entries found.")
return Table([])

tbl = Table(self.data["outreachEntries"])
for col in [
"outreachCreatedMts",
"outreachSnoozeUntilMts",
"outreachScheduledFollowUpMts",
]:
tbl.convert_column(col, lambda x: convert_unix_to_readable(x))
return tbl

def get_full_export(self):
"""
Get a table of the complete, raw data as returned by the API.
Meant to facilitate pure ELT pipelines

`Returns:`
Parsons Table
See :ref:`parsons-table` for output options.
"""

tbl = Table([self.data])
return tbl
11 changes: 11 additions & 0 deletions parsons/utilities/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ def date_to_timestamp(value, tzinfo=datetime.timezone.utc):
return int(parsed_date.timestamp())


def convert_unix_to_readable(ts):
"""
Converts UNIX timestamps to readable timestamps.
"""

ts = datetime.utcfromtimestamp(int(ts) / 1000)
ts = ts.strftime("%Y-%m-%d %H:%M:%S UTC")

return ts


def parse_date(value, tzinfo=datetime.timezone.utc):
"""Parse an arbitrary date value into a Python datetime.

Expand Down
Loading
Loading