diff --git a/docs/bluelink.rst b/docs/bluelink.rst deleted file mode 100644 index 985540a62..000000000 --- a/docs/bluelink.rst +++ /dev/null @@ -1,69 +0,0 @@ -Bluelink -============= - -******** -Overview -******** - -`Bluelink `_ is an online tool for connecting various `digital software tools `_ used by campaigns and movement groups in the political and non-profit space so you can sync data between them. This integration currently supports sending your structured person data and related tags to Bluelink via the `Bluelink Webhook API `_, after which you can use Bluelink's UI to send to any of their `supported tools `_. If you don't see a tool you would like to connect to, please reach out at hello@bluelink.org to ask them to add it. - -.. note:: - Authentication - If you don't have a Bluelink account please complete the `form `_ on the Bluelink website or email them at hello@bluelink.org. To get connection credentials select `Bluelink Webhook `_ from the apps menu. If you don't see this option, you may need to ask an account administrator to do this step for you. - - The credentials are automatically embedded into a one time secret link in case they need to be sent to you. Open the link to access the user and password. - -========== -Quickstart -========== - -To instantiate a class, you can either pass in the user and password token as arguments or set them in the -BLUELINK_WEBHOOK_USER and BLUELINK_WEBHOOK_PASSWORD environment variables. - -.. code-block:: python - - from parsons.bluelink import Bluelink - - # First approach: Use API credentials via environmental variables - bluelink = Bluelink() - - # Second approach: Pass API credentials as arguments - bluelink = Bluelink('username', 'password') - -You can upsert person data by directly using a BluelinkPerson object: - -.. code-block:: python - - from parsons.bluelink import Bluelink, BluelinkPerson, BluelinkIdentifier - - # create the person object - person = BluelinkPerson(identifiers=[BluelinkIdentifier(source="SOURCE_VENDOR", identifier="ID")], given_name="Jane", family_name="Doe") - - # use the bluelink connector to upsert - source = "MY_ORG_NAME" - bluelink.upsert_person(source, person) - -You can bulk upsert person data via a Parsons Table by providing a function that takes a row and outputs a BluelinkPerson: - -.. code-block:: python - - from parsons.bluelink import Bluelink, BluelinkPerson, BluelinkIdentifier - - # a function that takes a row and returns a BluelinkPerson - def row_to_person(row): - return BluelinkPerson(identifiers=[BluelinkIdentifier(source="SOURCE_VENDOR", identifier=row["id"])], - given_name=row["firstName"], family_name=row["lastName"]) - - # a parsons table filled with person data - parsons_tbl = get_data() - - # call bulk_upsert_person - source = "MY_ORG_NAME" - bluelink.bulk_upsert_person(source, parsons_tbl, row_to_person) - -*** -API -*** - -.. autoclass :: parsons.bluelink.Bluelink - :inherited-members: diff --git a/docs/index.rst b/docs/index.rst index b3cba40fc..bb057fbca 100755 --- a/docs/index.rst +++ b/docs/index.rst @@ -189,7 +189,6 @@ Indices and tables azure bill_com bloomerang - bluelink box braintree capitolcanary diff --git a/parsons/__init__.py b/parsons/__init__.py index 6762e54e2..ebc743e37 100644 --- a/parsons/__init__.py +++ b/parsons/__init__.py @@ -39,7 +39,6 @@ ("parsons.azure.azure_blob_storage", "AzureBlobStorage"), ("parsons.bill_com.bill_com", "BillCom"), ("parsons.bloomerang.bloomerang", "Bloomerang"), - ("parsons.bluelink", "Bluelink"), ("parsons.box.box", "Box"), ("parsons.braintree.braintree", "Braintree"), ("parsons.capitol_canary.capitol_canary", "CapitolCanary"), diff --git a/parsons/bluelink/__init__.py b/parsons/bluelink/__init__.py deleted file mode 100644 index d3c77bf6f..000000000 --- a/parsons/bluelink/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from parsons.bluelink.bluelink import Bluelink -from parsons.bluelink.person import ( - BluelinkPerson, - BluelinkEmail, - BluelinkAddress, - BluelinkPhone, - BluelinkIdentifier, - BluelinkTag, - BluelinkScore, -) - -__all__ = [ - "Bluelink", - "BluelinkPerson", - "BluelinkEmail", - "BluelinkAddress", - "BluelinkPhone", - "BluelinkIdentifier", - "BluelinkTag", - "BluelinkScore", -] diff --git a/parsons/bluelink/bluelink.py b/parsons/bluelink/bluelink.py deleted file mode 100644 index 93e2f933d..000000000 --- a/parsons/bluelink/bluelink.py +++ /dev/null @@ -1,84 +0,0 @@ -from parsons.utilities.api_connector import APIConnector -from parsons.utilities import check_env -from parsons.bluelink.person import BluelinkPerson -import logging -import json - -logger = logging.getLogger(__name__) - -API_URL = "https://api.bluelink.org/webhooks/" - - -class Bluelink: - """ - Instantiate a Bluelink connector. - Allows for a simple method of inserting person data to Bluelink via a webhook. - # see: https://bluelinkdata.github.io/docs/BluelinkApiGuide#webhook - - `Args:`: - user: str - Bluelink webhook user name. - password: str - Bluelink webhook password. - """ - - def __init__(self, user=None, password=None): - self.user = check_env.check("BLUELINK_WEBHOOK_USER", user) - self.password = check_env.check("BLUELINK_WEBHOOK_PASSWORD", password) - self.headers = { - "Content-Type": "application/json", - } - self.api_url = API_URL - self.api = APIConnector(self.api_url, auth=(self.user, self.password), headers=self.headers) - - def upsert_person(self, source, person=None): - """ - Upsert a BluelinkPerson object into Bluelink. - Rows will update, as opposed to being inserted, if an existing person record in - Bluelink has a matching BluelinkIdentifier (same source and id) as the BluelinkPerson object - passed into this function. - - `Args:` - source: str - String to identify that the data came from your system. For example, - your company name. - person: BluelinkPerson - A BluelinkPerson object. - Will be inserted to Bluelink, or updated if a matching record is found. - `Returns:` - int - An http status code from the http post request to the Bluelink webhook. - """ - data = {"source": source, "person": person} - jdata = json.dumps( - data, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, - ) - resp = self.api.post_request(url=self.api_url, data=jdata) - return resp - - def bulk_upsert_person(self, source, tbl, row_to_person): - """ - Upsert all rows into Bluelink, using the row_to_person function to - transform rows to BluelinkPerson objects. - - `Args:` - source: str - String to identify that the data came from your system. - For example, your company name. - tbl: Table - A parsons Table that represents people data. - row_to_person: Callable[[dict],BluelinkPerson] - A function that takes a dict representation of a row from the passed in tbl - and returns a BluelinkPerson object. - - `Returns:` - list[int] - A list of https response status codes, one response for each row in the table. - """ - people = BluelinkPerson.from_table(tbl, row_to_person) - responses = [] - for person in people: - response = self.upsert_person(source, person) - responses.append(response) - return responses diff --git a/parsons/bluelink/person.py b/parsons/bluelink/person.py deleted file mode 100644 index f808c090f..000000000 --- a/parsons/bluelink/person.py +++ /dev/null @@ -1,280 +0,0 @@ -import logging -import json - -logger = logging.getLogger(__name__) - - -class BluelinkPerson(object): - """ - Instantiate BluelinkPerson Class. - Used for to upserting via Bluelink connector. - See: https://bluelinkdata.github.io/docs/BluelinkApiGuide#person-object - - `Args:` - identifiers: list[BluelinkIdentifier] - A list of BluelinkIdentifier objects. - A BluelinkPerson must have at least 1 identifier. - given_name: str - First name / given name. - family_name: str - Last name / family name. - phones: list[BluelinkPhone] - A list of BluelinkPhone objects representing phone numbers. - emails: list[BluelinkEmail] - A list of BluelinkEmail objects representing email addresses. - addresses: list[BluelinkAddress] - A list of BluelinkAddress objects representing postal addresses. - tags: list[BluelinkTag] - Simple tags that apply to the person, eg DONOR. - employer: str - Name of the persons employer. - employer_address: BluelinkAddress - BluelinkAddress of the persons employer. - occupation: str - Occupation. - scores: list[BluelinkScore] - List of BluelinkScore objects. Scores are numeric scores, ie partisanship model. - birthdate: str - ISO 8601 formatted birth date: 'YYYY-MM-DD' - details: dict - additional custom data. must be json serializable. - """ - - def __init__( - self, - identifiers, - given_name=None, - family_name=None, - phones=None, - emails=None, - addresses=None, - tags=None, - employer=None, - employer_address=None, - occupation=None, - scores=None, - birthdate=None, - details=None, - ): - - if not identifiers: - raise Exception( - "BluelinkPerson requires list of BluelinkIdentifiers with " - "at least 1 BluelinkIdentifier" - ) - - self.identifiers = identifiers - self.addresses = addresses - self.emails = emails - self.phones = phones - self.tags = tags - self.scores = scores - - self.given_name = given_name - self.family_name = family_name - - self.employer = employer - self.employer_address = employer_address - self.occupation = occupation - self.birthdate = birthdate - self.details = details - - def __json__(self): - """The json str representation of this BluelinkPerson object""" - return json.dumps(self, default=lambda obj: obj.__dict__) - - def __eq__(self, other): - """A quick and dirty equality check""" - dself = json.loads(self.__json__()) - dother = json.loads(other.__json__()) - return dself == dother - - def __repr__(self): - return self.__json__() - - @staticmethod - def from_table(tbl, dict_to_person): - """ - Return a list of BluelinkPerson objects from a Parsons Table. - - `Args:` - tbl: Table - A parsons Table. - dict_to_person: Callable[[dict],BluelinkPerson] - A function that takes a dictionary representation of a table row, - and returns a BluelinkPerson. - `Returns:` - list[BluelinkPerson] - A list of BluelinkPerson objects. - """ - return [dict_to_person(row) for row in tbl] - - -class BluelinkIdentifier(object): - """ - Instantiate an BluelinkIdentifier object. - BluelinkIdentifier is necessary for updating BluelinkPerson records. - - `Args:` - source: str - External system to which this ID belongs, e.g., “VAN:myCampaign”. - Bluelink has standardized strings for source. Using these will - allow Bluelink to correctly understand the external IDs you add. - source (unlike identifier) is case insensitive. - examples: BLUELINK, PDI, SALESFORCE, VAN:myCampaign, VAN:myVoters - identifier: str - Case-sensitive ID in the external system. - details: dict - dictionary of custom fields. must be serializable to json. - """ - - def __init__(self, source, identifier, details=None): - self.source = source - self.identifier = identifier - self.details = details - - -class BluelinkEmail(object): - """ - Instantiate an BluelinkEmail object. - - `Args:` - address: str - An email address. ie "user@example.com" - primary: bool - True if this is known to be the primary email. - type: str - Type, eg: "personal", "work" - status: str - One of "Potential", "Subscribed", "Unsubscribed", "Bouncing", or "Spam Complaints" - """ - - def __init__(self, address, primary=None, type=None, status=None): - self.address = address - self.primary = primary - self.type = type - self.status = status - - -class BluelinkAddress(object): - """ - Instantiate an BluelinkAddress object. - - `Args`: - address_lines: list[str] - A list of street address lines. - city: str - City or other locality. - state: str - State in ISO 3166-2. - postal_code: str - Zip or other postal code. - country: str - ISO 3166-1 Alpha-2 country code. - type: str - The type. ie: "home", "mailing". - venue: str - The venue name, if relevant. - status: str - A value representing the status of the address. "Potential", "Verified" or "Bad" - """ - - def __init__( - self, - address_lines=None, - city=None, - state=None, - postal_code=None, - country=None, - type=None, - venue=None, - status=None, - ): - - self.address_lines = address_lines or [] - self.city = city - self.state = state - self.postal_code = postal_code - self.country = country - - self.type = type - self.venue = venue - self.status = status - - -class BluelinkPhone(object): - """ - Instantiate a BluelinkPhone object. - - `Args:` - number: str - A phone number. May or may not include country code. - primary: bool - True if this is known to be the primary phone. - description: str - Free for description. - type: str - Type, eg: "Home", "Work", "Mobile" - country: str - ISO 3166-1 Alpha-2 country code. - sms_capable: bool - True if this number can accept SMS. - do_not_call: bool - True if this number is on the US FCC Do Not Call Registry. - details: dict - Additional data dictionary. Must be json serializable. - """ - - def __init__( - self, - number, - primary=None, - description=None, - type=None, - country=None, - sms_capable=None, - do_not_call=None, - details=None, - ): - self.number = number - self.primary = primary - self.description = description - self.type = type - self.country = country - self.sms_capable = sms_capable - self.do_not_call = do_not_call - self.details = details - - -class BluelinkTag(object): - """ - Instantiate a BluelinkTag object. - - `Args:` - tag: str - A tag string; convention is either a simple string - or a string with a prefix separated by a colon, e.g., “DONOR:GRASSROOTS” - """ - - def __init__(self, tag): - self.tag = tag - - -class BluelinkScore(object): - """ - Instantiate a score object. - Represents some kind of numeric score. - - `Args`: - score: float - Numeric score. - score_type: str - Type, eg: "Partisanship model". - source: str - Original source of this score. - """ - - def __init__(self, score, score_type, source): - self.score = score - self.score_type = score_type - self.source = source diff --git a/test/test_bluelink/test_bluelink.py b/test/test_bluelink/test_bluelink.py deleted file mode 100644 index 5c15ea5f7..000000000 --- a/test/test_bluelink/test_bluelink.py +++ /dev/null @@ -1,117 +0,0 @@ -import unittest -import requests_mock -from parsons import Table, Bluelink -from parsons.bluelink import BluelinkPerson, BluelinkIdentifier, BluelinkEmail - - -class TestBluelink(unittest.TestCase): - @requests_mock.Mocker() - def setUp(self, m): - self.bluelink = Bluelink("fake_user", "fake_password") - - @staticmethod - def row_to_person(row): - """ - dict -> BluelinkPerson - Transforms a parsons Table row to a BluelinkPerson. - This function is passed into bulk_upsert_person along with a Table - """ - email = row["email"] - return BluelinkPerson( - identifiers=[ - BluelinkIdentifier(source="FAKESOURCE", identifier=email), - ], - emails=[BluelinkEmail(address=email, primary=True)], - family_name=row["family_name"], - given_name=row["given_name"], - ) - - @staticmethod - def get_table(): - return Table( - [ - { - "given_name": "Bart", - "family_name": "Simpson", - "email": "bart@springfield.net", - }, - { - "given_name": "Homer", - "family_name": "Simpson", - "email": "homer@springfield.net", - }, - ] - ) - - @requests_mock.Mocker() - def test_bulk_upsert_person(self, m): - """ - This function demonstrates how to use a "row_to_person" function to bulk - insert people using a Table as the data source - """ - # Mock POST requests to api - m.post(self.bluelink.api_url) - - # get data as a parsons Table - tbl = self.get_table() - - # String to identify that the data came from your system. For example, your company name. - source = "BLUELINK-PARSONS-TEST" - - # call bulk_upsert_person - # passing in the source, the Table, and the function that maps a Table row -> BluelinkPerson - self.bluelink.bulk_upsert_person(source, tbl, self.row_to_person) - - @requests_mock.Mocker() - def test_upsert_person(self, m): - """ - This function demonstrates how to insert a single BluelinkPerson record - """ - # Mock POST requests to api - m.post(self.bluelink.api_url) - - # create a BluelinkPerson object - # The BluelinkIdentifier is pretending that the user can be - # identified in SALESFORCE with FAKE_ID as her id - person = BluelinkPerson( - identifiers=[BluelinkIdentifier(source="SALESFORCE", identifier="FAKE_ID")], - given_name="Jane", - family_name="Doe", - emails=[BluelinkEmail(address="jdoe@example.com", primary=True)], - ) - - # String to identify that the data came from your system. For example, your company name. - source = "BLUELINK-PARSONS-TEST" - - # call upsert_person - self.bluelink.upsert_person(source, person) - - def test_table_to_people(self): - """ - Test transforming a parsons Table -> list[BluelinkPerson] - """ - # setup - tbl = self.get_table() - - # function under test - actual_people = BluelinkPerson.from_table(tbl, self.row_to_person) - - # expected: - person1 = BluelinkPerson( - identifiers=[ - BluelinkIdentifier(source="FAKESOURCE", identifier="bart@springfield.net") - ], - emails=[BluelinkEmail(address="bart@springfield.net", primary=True)], - family_name="Simpson", - given_name="Bart", - ) - person2 = BluelinkPerson( - identifiers=[ - BluelinkIdentifier(source="FAKESOURCE", identifier="homer@springfield.net") - ], - emails=[BluelinkEmail(address="homer@springfield.net", primary=True)], - family_name="Simpson", - given_name="Homer", - ) - expected_people = [person1, person2] - self.assertEqual(actual_people, expected_people)