Skip to content

Commit 564ab1e

Browse files
Enrich data with Mailchimp (#14)
1 parent f6913bb commit 564ab1e

File tree

10 files changed

+90
-61
lines changed

10 files changed

+90
-61
lines changed

.github/workflows/cron.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ jobs:
2424
SALESFORCE_PASSWORD: ${{ secrets.SALESFORCE_PASSWORD }}
2525
SALESFORCE_TOKEN: ${{ secrets.SALESFORCE_TOKEN }}
2626
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
27+
MAILCHIMP_KEY: ${{ secrets.MAILCHIMP_KEY }}
28+
MAILCHIMP_LIST_ID: ${{ secrets.MAILCHIMP_LIST_ID }}

default.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// "generated_with_requirements": [
1212
// "cryptography",
1313
// "geopy",
14+
// "mailchimp3",
1415
// "pydantic",
1516
// "pytest",
1617
// "python-Levenshtein",
@@ -1483,6 +1484,26 @@
14831484
"requires_python": ">=3.6",
14841485
"version": "5.2.2"
14851486
},
1487+
{
1488+
"artifacts": [
1489+
{
1490+
"algorithm": "sha256",
1491+
"hash": "6eb335fcd915b3233285a0bdf6317fdfc412b4a638c102ae3a7a52019d3f0970",
1492+
"url": "https://files.pythonhosted.org/packages/5c/a9/772609122afc83ac149bb03ec92e2e91659b3d274e8dbfd0cd02ec5e78f7/mailchimp3-3.0.21-py2.py3-none-any.whl"
1493+
},
1494+
{
1495+
"algorithm": "sha256",
1496+
"hash": "5e2930ece6144abb659d45e692e92135ab05c9027d3f5e807c0f66cfb374b9ad",
1497+
"url": "https://files.pythonhosted.org/packages/08/71/cdef8e888784c5da1186ce82e8d3414dc3d8e47a9fe7b62b483e217591eb/mailchimp3-3.0.21.tar.gz"
1498+
}
1499+
],
1500+
"project_name": "mailchimp3",
1501+
"requires_dists": [
1502+
"requests>=2.7.0"
1503+
],
1504+
"requires_python": null,
1505+
"version": "3.0.21"
1506+
},
14861507
{
14871508
"artifacts": [
14881509
{
@@ -2604,6 +2625,7 @@
26042625
"requirements": [
26052626
"cryptography",
26062627
"geopy",
2628+
"mailchimp3",
26072629
"pydantic",
26082630
"pytest",
26092631
"python-Levenshtein",

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[[tool.mypy.overrides]]
22
module = [
33
"geopy",
4+
"mailchimp3",
45
"uszipcode"
56
]
67
ignore_missing_imports = true

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
cryptography
22
geopy
3-
uszipcode
3+
mailchimp3
44
python-Levenshtein
55
pydantic
66
pytest
77
simple-salesforce
8+
uszipcode

src/mailchimp_coordinates.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
import os
3+
from typing import Any, NamedTuple
4+
5+
from mailchimp3 import MailChimp
6+
7+
logging.getLogger("mailchimp3.client").setLevel(logging.CRITICAL)
8+
9+
10+
class Coordinates(NamedTuple):
11+
latitude: float
12+
longitude: float
13+
14+
@classmethod
15+
def from_mailchimp(cls, entry: dict[str, Any]) -> "Coordinates | None":
16+
lat: float = entry["location"]["latitude"]
17+
long: float = entry["location"]["longitude"]
18+
return cls(lat, long) if lat and long else None
19+
20+
21+
def get_coordinates_by_email() -> dict[str, Coordinates | None]:
22+
key = os.environ.pop("MAILCHIMP_KEY")
23+
list_id = os.environ.pop("MAILCHIMP_LIST_ID")
24+
client = MailChimp(mc_api=key)
25+
result = client.lists.members.all(
26+
list_id=list_id,
27+
fields="members.email_address,members.location.latitude,members.location.longitude",
28+
get_all=True,
29+
)["members"]
30+
return {
31+
entry["email_address"]: coords
32+
for entry in result
33+
if (coords := Coordinates.from_mailchimp(entry)) is not None
34+
}

src/mailchimp_entry.py

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/main.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import metro_csvs
88
import salesforce_api
9-
from mailchimp_entry import MailchimpEntry
9+
from mailchimp_coordinates import get_coordinates_by_email
1010

1111
logger = logging.getLogger(__name__)
1212
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
@@ -25,9 +25,10 @@ def main() -> None:
2525

2626
salesforce_client = salesforce_api.init_client()
2727
entries = salesforce_api.load_data(salesforce_client)
28+
logger.info(f"Loaded {len(entries)} Salesforce records")
2829

29-
# TODO: read in Mailchimp data
30-
mailchimp_by_email: dict[str, MailchimpEntry] = {}
30+
coordinates_by_email = get_coordinates_by_email()
31+
logger.info(f"Loaded {len(coordinates_by_email)} coordinates from Mailchimp")
3132

3233
us_zip_to_metro = metro_csvs.read_us_zip_to_metro()
3334
us_city_and_state_to_metro = metro_csvs.read_us_city_and_state_to_metro()
@@ -40,8 +41,8 @@ def main() -> None:
4041

4142
# The order of operations matters.
4243
if entry.email:
43-
entry.populate_via_latitude_longitude(
44-
mailchimp_by_email.get(entry.email), geocoder
44+
entry.populate_via_coordinates(
45+
coordinates_by_email.get(entry.email), geocoder
4546
)
4647
entry.normalize()
4748
entry.populate_via_zipcode(zipcode_search_engine)

src/salesforce_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77

88
def init_client() -> Salesforce:
9-
USERNAME = os.environ.pop("SALESFORCE_USERNAME")
10-
PASSWORD = os.environ.pop("SALESFORCE_PASSWORD")
11-
TOKEN = os.environ.pop("SALESFORCE_TOKEN")
9+
username = os.environ.pop("SALESFORCE_USERNAME")
10+
password = os.environ.pop("SALESFORCE_PASSWORD")
11+
token = os.environ.pop("SALESFORCE_TOKEN")
1212
return Salesforce(
13-
username=USERNAME,
14-
password=PASSWORD,
15-
security_token=TOKEN,
13+
username=username,
14+
password=password,
15+
security_token=token,
1616
client_id="salesforce-data-enrichment",
1717
)
1818

src/salesforce_entry.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from uszipcode import SearchEngine
33
from pydantic import BaseModel, Field
44

5-
from mailchimp_entry import MailchimpEntry
5+
from mailchimp_coordinates import Coordinates
66
from country_codes import COUNTRY_CODES_TWO_LETTER_TO_THREE, COUNTRY_NAMES_TO_THREE
77
from state_codes import US_STATES_TO_CODES
88

@@ -12,8 +12,8 @@ class SalesforceEntry(BaseModel):
1212
email: str | None = Field(..., alias="Email", frozen=True)
1313
city: str | None = Field(..., alias="MailingCity")
1414
country: str | None = Field(..., alias="MailingCountry")
15-
latitude: str | None = Field(..., alias="MailingLatitude")
16-
longitude: str | None = Field(..., alias="MailingLongitude")
15+
latitude: float | None = Field(..., alias="MailingLatitude")
16+
longitude: float | None = Field(..., alias="MailingLongitude")
1717
zipcode: str | None = Field(..., alias="MailingPostalCode")
1818
state: str | None = Field(..., alias="MailingState")
1919
street: str | None = Field(..., alias="MailingStreet")
@@ -25,8 +25,8 @@ def mock(
2525
*,
2626
city: str | None = None,
2727
country: str | None = None,
28-
latitude: str | None = None,
29-
longitude: str | None = None,
28+
latitude: float | None = None,
29+
longitude: float | None = None,
3030
zipcode: str | None = None,
3131
state: str | None = None,
3232
street: str | None = None,
@@ -83,24 +83,24 @@ def normalize(self) -> None:
8383
raise AssertionError(f"Unexpected zipcode for {self}")
8484
self.zipcode = self.zipcode[:5]
8585

86-
def populate_via_latitude_longitude(
87-
self, mailchimp: MailchimpEntry | None, geocoder: Nominatim
86+
def populate_via_coordinates(
87+
self, coordinates: Coordinates | None, geocoder: Nominatim
8888
) -> None:
89-
if mailchimp is None or not (mailchimp.latitude and mailchimp.longitude):
89+
if coordinates is None:
9090
return
9191

9292
metro_area_can_be_computed = self.zipcode or (self.city and self.country)
9393
if metro_area_can_be_computed:
9494
return
9595

96-
addr = geocoder.reverse(f"{mailchimp.latitude}, {mailchimp.longitude}").raw[
96+
addr = geocoder.reverse(f"{coordinates.latitude}, {coordinates.longitude}").raw[
9797
"address"
9898
]
9999
if "postcode" not in addr:
100100
return
101101

102-
self.latitude = mailchimp.latitude
103-
self.longitude = mailchimp.longitude
102+
self.latitude = coordinates.latitude
103+
self.longitude = coordinates.longitude
104104
self.zipcode = addr["postcode"]
105105

106106
# Also overwrite any existing values so that we don't mix the prior address

src/salesforce_entry_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44
from uszipcode import SearchEngine
55

6-
from mailchimp_entry import MailchimpEntry
6+
from mailchimp_coordinates import Coordinates
77
from salesforce_entry import SalesforceEntry
88

99

@@ -95,16 +95,16 @@ def test_populate_via_zipcode(
9595
assert entry.city == expected_city
9696

9797

98-
def test_populate_via_lat_long(geocoder_mock) -> None:
99-
mailchimp = MailchimpEntry.mock(latitude="1.1", longitude="'4.2")
98+
def test_populate_via_coordinates(geocoder_mock) -> None:
99+
coordinates = Coordinates(latitude=1.1, longitude=4.2)
100100
entry = SalesforceEntry.mock()
101-
entry.populate_via_latitude_longitude(mailchimp, geocoder_mock)
101+
entry.populate_via_coordinates(coordinates, geocoder_mock)
102102
assert entry.city == "New York"
103103
assert entry.state == "NY"
104104
assert entry.country == "USA"
105105
assert entry.zipcode == "11370"
106-
assert entry.latitude == "1.1"
107-
assert entry.longitude == "4.2"
106+
assert entry.latitude == 1.1
107+
assert entry.longitude == 4.2
108108

109109

110110
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)