Skip to content

Commit 3740fd4

Browse files
authored
Merge pull request #39 from IABTechLab/ccm-UID2-2760-implement-identity-map
UID2-2760 Add IdentityMapClient
2 parents 252b57e + ff74116 commit 3740fd4

File tree

9 files changed

+410
-22
lines changed

9 files changed

+410
-22
lines changed

Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM python:3.6
1+
FROM python:3.8
22
COPY . /build
33
RUN pip install --no-cache-dir -e /build[dev]

README.md

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,12 @@ For documentation on usage, see the [UID2 SDK for Python Reference Guide](https:
2626

2727
## Example Usage
2828

29-
To run all the example applications:
3029

31-
```
32-
make examples BASE_URL=https://prod.uidapi.com AUTH_KEY=my-auth-key SECRET_KEY=my-secret-key \
33-
AD_TOKEN=AgAAAANRdREk+IWqqnQkZ2rZdK0TgSUP/owLryysSkUGZJT+Gy551L1WJMAZA/G2B1UMDQ20WAqwwTu6o9TexWyux0lg0HHIbmJjN6IYwo+42KC8ugaR+PX0y18qQ+3yzkxmJ/ee//4IGu/1Yq4AmO4ArXN6CeszPTxByTkysVqyQVNY2A== \
34-
RAW_UID=JCqmlLXpbbu/jTdpB2a1cNAVs8O72eMXPaQzC9Ic9mE= \
35-
DOMAIN=example.com
36-
```
37-
38-
Alternatively, you can run specific examples:
30+
You can run specific examples:
3931

4032
```
41-
make example_client BASE_URL=https://prod.uidapi.com AUTH_KEY=my-auth-key SECRET_KEY=my-secret-key \
42-
AD_TOKEN=AgAAAANRdREk+IWqqnQkZ2rZdK0TgSUP/owLryysSkUGZJT+Gy551L1WJMAZA/G2B1UMDQ20WAqwwTu6o9TexWyux0lg0HHIbmJjN6IYwo+42KC8ugaR+PX0y18qQ+3yzkxmJ/ee//4IGu/1Yq4AmO4ArXN6CeszPTxByTkysVqyQVNY2A==
43-
make example_auto_refresh BASE_URL=https://prod.uidapi.com AUTH_KEY=my-auth-key SECRET_KEY=my-secret-key \
44-
AD_TOKEN=AgAAAANRdREk+IWqqnQkZ2rZdK0TgSUP/owLryysSkUGZJT+Gy551L1WJMAZA/G2B1UMDQ20WAqwwTu6o9TexWyux0lg0HHIbmJjN6IYwo+42KC8ugaR+PX0y18qQ+3yzkxmJ/ee//4IGu/1Yq4AmO4ArXN6CeszPTxByTkysVqyQVNY2A==
33+
python examples/sample_bidstream_client.py BASE_URL=https://operator-integ.uidapi.com AUTH_KEY=my-auth-key SECRET_KEY=my-secret-key
34+
DOMAIN_NAME=domain-name AD_TOKEN=ad-token
4535
```
4636

4737
## Development
@@ -52,12 +42,6 @@ First, build the Docker image with Python 3.6 and all dev dependencies. This is
5242
make docker
5343
```
5444

55-
Run unit tests:
56-
57-
```
58-
make test
59-
```
60-
6145
Build a bdist wheel:
6246

6347
```
@@ -69,3 +53,4 @@ Get access to an interactive shell within the Python 3.6 Docker image:
6953
```
7054
make shell
7155
```
56+
Run unit tests: Use PyCharm to run the test cases
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import sys
2+
3+
from uid2_client import IdentityMapClient, IdentityMapInput
4+
5+
6+
# this sample client takes email addresses as input and generates an IdentityMapResponse object which contains raw uid
7+
# or the reason why it is unmapped
8+
9+
def _usage():
10+
print('Usage: python3 sample_identity_map_client.py <base_url> <api_key> <client_secret> <email_1> <email_2> ... <email_n>'
11+
, file=sys.stderr)
12+
sys.exit(1)
13+
14+
15+
if len(sys.argv) <= 4:
16+
_usage()
17+
18+
base_url = sys.argv[1]
19+
api_key = sys.argv[2]
20+
client_secret = sys.argv[3]
21+
email_list = sys.argv[4:]
22+
first_email = sys.argv[4]
23+
24+
client = IdentityMapClient(base_url, api_key, client_secret)
25+
26+
identity_map_response = client.generate_identity_map(IdentityMapInput.from_emails(email_list))
27+
28+
mapped_identities = identity_map_response.mapped_identities
29+
unmapped_identities = identity_map_response.unmapped_identities
30+
31+
mapped_identity = mapped_identities.get(first_email)
32+
if mapped_identity is not None:
33+
raw_uid = mapped_identity.get_raw_uid()
34+
print('raw_uid =', raw_uid)
35+
else:
36+
unmapped_identity = unmapped_identities.get(first_email)
37+
reason = unmapped_identity.get_reason()
38+
print('reason =', reason)

tests/test_identity_map_client.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import os
2+
import unittest
3+
from urllib.error import URLError, HTTPError
4+
5+
from uid2_client import IdentityMapClient, IdentityMapInput, normalize_and_hash_email, normalize_and_hash_phone
6+
7+
8+
class IdentityMapIntegrationTests(unittest.TestCase):
9+
UID2_BASE_URL = None
10+
UID2_API_KEY = None
11+
UID2_SECRET_KEY = None
12+
13+
identity_map_client = None
14+
15+
@classmethod
16+
def setUpClass(cls):
17+
cls.UID2_BASE_URL = os.getenv("UID2_BASE_URL")
18+
cls.UID2_API_KEY = os.getenv("UID2_API_KEY")
19+
cls.UID2_SECRET_KEY = os.getenv("UID2_SECRET_KEY")
20+
21+
if cls.UID2_BASE_URL and cls.UID2_API_KEY and cls.UID2_SECRET_KEY:
22+
cls.identity_map_client = IdentityMapClient(cls.UID2_BASE_URL, cls.UID2_API_KEY, cls.UID2_SECRET_KEY)
23+
else:
24+
raise Exception("set the required UID2_BASE_URL/UID2_API_KEY/UID2_SECRET_KEY environment variables first")
25+
26+
def test_identity_map_emails(self):
27+
identity_map_input = IdentityMapInput.from_emails(
28+
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
29+
response = self.identity_map_client.generate_identity_map(identity_map_input)
30+
self.assert_mapped(response, "hopefully-not-opted-out@example.com")
31+
self.assert_mapped(response, "somethingelse@example.com")
32+
33+
self.assert_unmapped(response, "optout", "optout@example.com")
34+
35+
def test_identity_map_nothing_unmapped(self):
36+
identity_map_input = IdentityMapInput.from_emails(
37+
["hopefully-not-opted-out@example.com", "somethingelse@example.com"])
38+
response = self.identity_map_client.generate_identity_map(identity_map_input)
39+
self.assert_mapped(response, "hopefully-not-opted-out@example.com")
40+
self.assert_mapped(response, "somethingelse@example.com")
41+
42+
def test_identity_map_nothing_mapped(self):
43+
identity_map_input = IdentityMapInput.from_emails(["optout@example.com"])
44+
response = self.identity_map_client.generate_identity_map(identity_map_input)
45+
self.assert_unmapped(response, "optout", "optout@example.com")
46+
47+
def test_identity_map_invalid_email(self):
48+
self.assertRaises(ValueError, IdentityMapInput.from_emails,
49+
["email@example.com", "this is not an email"])
50+
51+
def test_identity_map_invalid_phone(self):
52+
self.assertRaises(ValueError, IdentityMapInput.from_phones,
53+
["+12345678901", "this is not a phone number"])
54+
55+
def test_identity_map_invalid_hashed_email(self):
56+
identity_map_input = IdentityMapInput.from_hashed_emails(["this is not a hashed email"])
57+
response = self.identity_map_client.generate_identity_map(identity_map_input)
58+
self.assert_unmapped(response, "invalid identifier", "this is not a hashed email")
59+
60+
def test_identity_map_invalid_hashed_phone(self):
61+
identity_map_input = IdentityMapInput.from_hashed_emails(["this is not a hashed phone"])
62+
response = self.identity_map_client.generate_identity_map(identity_map_input)
63+
self.assert_unmapped(response, "invalid identifier", "this is not a hashed phone")
64+
65+
def test_identity_map_hashed_emails(self):
66+
hashed_email1 = normalize_and_hash_email("hopefully-not-opted-out@example.com")
67+
hashed_email2 = normalize_and_hash_email("somethingelse@example.com")
68+
hashed_opted_out_email = normalize_and_hash_email("optout@example.com")
69+
identity_map_input = IdentityMapInput.from_hashed_emails([hashed_email1, hashed_email2, hashed_opted_out_email])
70+
71+
response = self.identity_map_client.generate_identity_map(identity_map_input)
72+
73+
self.assert_mapped(response, hashed_email1)
74+
self.assert_mapped(response, hashed_email2)
75+
76+
self.assert_unmapped(response, "optout", hashed_opted_out_email)
77+
78+
def test_identity_map_duplicate_emails(self):
79+
identity_map_input = IdentityMapInput.from_emails(
80+
["JANE.SAOIRSE@gmail.com", "Jane.Saoirse@gmail.com", "JaneSaoirse+UID2@gmail.com", "janesaoirse@gmail.com",
81+
"JANE.SAOIRSE@gmail.com"])
82+
response = self.identity_map_client.generate_identity_map(identity_map_input)
83+
84+
mapped_identities = response.mapped_identities
85+
self.assertEqual(4, len(mapped_identities))
86+
87+
raw_uid = mapped_identities.get("JANE.SAOIRSE@gmail.com").get_raw_uid()
88+
self.assertEqual(raw_uid, mapped_identities.get("Jane.Saoirse@gmail.com").get_raw_uid())
89+
self.assertEqual(raw_uid, mapped_identities.get("JaneSaoirse+UID2@gmail.com").get_raw_uid())
90+
self.assertEqual(raw_uid, mapped_identities.get("janesaoirse@gmail.com").get_raw_uid())
91+
92+
def test_identity_map_duplicate_hashed_emails(self):
93+
hashed_email = normalize_and_hash_email("hopefully-not-opted-out@example.com")
94+
duplicate_hashed_email = hashed_email
95+
hashed_opted_out_email = normalize_and_hash_email("optout@example.com")
96+
duplicate_hashed_opted_out_email = hashed_opted_out_email
97+
98+
identity_map_input = IdentityMapInput.from_hashed_emails(
99+
[hashed_email, duplicate_hashed_email, hashed_opted_out_email, duplicate_hashed_opted_out_email])
100+
response = self.identity_map_client.generate_identity_map(identity_map_input)
101+
102+
self.assert_mapped(response, hashed_email)
103+
self.assert_mapped(response, duplicate_hashed_email)
104+
105+
self.assert_unmapped(response, "optout", hashed_opted_out_email)
106+
self.assert_unmapped(response, "optout", duplicate_hashed_opted_out_email)
107+
108+
def test_identity_map_empty_input(self):
109+
identity_map_input = IdentityMapInput.from_emails([])
110+
response = self.identity_map_client.generate_identity_map(identity_map_input)
111+
self.assertTrue(len(response.mapped_identities) == 0)
112+
self.assertTrue(len(response.unmapped_identities) == 0)
113+
114+
def test_identity_map_phones(self):
115+
identity_map_input = IdentityMapInput.from_phones(["+12345678901", "+98765432109", "+00000000000"])
116+
response = self.identity_map_client.generate_identity_map(identity_map_input)
117+
self.assert_mapped(response, "+12345678901")
118+
self.assert_mapped(response, "+98765432109")
119+
120+
self.assert_unmapped(response, "optout", "+00000000000")
121+
122+
def test_identity_map_hashed_phones(self):
123+
hashed_phone1 = normalize_and_hash_phone("+12345678901")
124+
hashed_phone2 = normalize_and_hash_phone("+98765432109")
125+
hashed_opted_out_phone = normalize_and_hash_phone("+00000000000")
126+
identity_map_input = IdentityMapInput.from_hashed_phones([hashed_phone1, hashed_phone2, hashed_opted_out_phone])
127+
response = self.identity_map_client.generate_identity_map(identity_map_input)
128+
self.assert_mapped(response, hashed_phone1)
129+
self.assert_mapped(response, hashed_phone2)
130+
131+
self.assert_unmapped(response, "optout", hashed_opted_out_phone)
132+
133+
def test_identity_map_bad_url(self):
134+
identity_map_input = IdentityMapInput.from_emails(
135+
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
136+
client = IdentityMapClient("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"), os.getenv("UID2_SECRET_KEY"))
137+
self.assertRaises(URLError, client.generate_identity_map, identity_map_input)
138+
139+
def test_identity_map_bad_api_key(self):
140+
identity_map_input = IdentityMapInput.from_emails(
141+
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
142+
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), "bad-api-key", os.getenv("UID2_SECRET_KEY"))
143+
self.assertRaises(HTTPError, client.generate_identity_map,identity_map_input)
144+
145+
def test_identity_map_bad_secret(self):
146+
identity_map_input = IdentityMapInput.from_emails(
147+
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
148+
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
149+
self.assertRaises(HTTPError, client.generate_identity_map,
150+
identity_map_input)
151+
152+
def assert_mapped(self, response, dii):
153+
mapped_identity = response.mapped_identities.get(dii)
154+
self.assertIsNotNone(mapped_identity)
155+
self.assertIsNotNone(mapped_identity.get_raw_uid())
156+
self.assertIsNotNone(mapped_identity.get_bucket_id())
157+
158+
unmapped_identity = response.unmapped_identities.get(dii)
159+
self.assertIsNone(unmapped_identity)
160+
161+
def assert_unmapped(self, response, reason, dii):
162+
unmapped_identity = response.unmapped_identities.get(dii)
163+
self.assertEqual(reason, unmapped_identity.get_reason())
164+
165+
mapped_identity = response.mapped_identities.get(dii)
166+
self.assertIsNone(mapped_identity)
167+
168+
169+
if __name__ == '__main__':
170+
unittest.main()

uid2_client/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
from .encryption_data_response import *
2525
from .refresh_response import *
2626
from .uid2_token_generator import *
27-
28-
27+
from .identity_map_client import *
28+
from .identity_map_input import *
29+
from .identity_map_response import *

uid2_client/identity_map_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import base64
2+
import datetime as dt
3+
from datetime import timezone
4+
5+
from .identity_map_response import IdentityMapResponse
6+
7+
from uid2_client import auth_headers, make_v2_request, post, parse_v2_response
8+
9+
10+
class IdentityMapClient:
11+
"""Client for interacting with UID Identity Map services
12+
13+
You will need to have the base URL of the endpoint and a client API key
14+
and secret to consume web services.
15+
16+
Methods:
17+
generate_identity_map: Generate identity map
18+
"""
19+
20+
def __init__(self, base_url, api_key, client_secret):
21+
"""Create a new IdentityMapClient client.
22+
23+
Args:
24+
base_url (str): base URL for all requests to UID services (e.g. 'https://prod.uidapi.com')
25+
api_key (str): api key for consuming the UID services
26+
client_secret (str): client secret for consuming the UID services
27+
28+
Note:
29+
Your authorization key will determine which UID services you are allowed to use.
30+
"""
31+
self._base_url = base_url
32+
self._api_key = api_key
33+
self._client_secret = base64.b64decode(client_secret)
34+
35+
def generate_identity_map(self, identity_map_input):
36+
req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc),
37+
identity_map_input.get_identity_map_input_as_json_string().encode())
38+
resp = post(self._base_url, '/v2/identity/map', headers=auth_headers(self._api_key), data=req)
39+
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
40+
return IdentityMapResponse(resp_body, identity_map_input)

uid2_client/identity_map_input.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import json
2+
3+
from uid2_client import IdentityType, normalize_and_hash_email, normalize_and_hash_phone
4+
5+
6+
class IdentityMapInput:
7+
"""input for IdentityMapClient, such as email addresses or phone numbers"""
8+
9+
def __init__(self, identity_type, emails_or_phones, already_hashed):
10+
self.hashed_dii_to_raw_diis = {}
11+
self.hashed_normalized_emails = None
12+
self.hashed_normalized_phones = None
13+
if identity_type == IdentityType.Email:
14+
if already_hashed:
15+
self.hashed_normalized_emails = emails_or_phones
16+
else:
17+
self.hashed_normalized_emails = []
18+
for email in emails_or_phones:
19+
hashed_normalized_email = normalize_and_hash_email(email)
20+
self._add_hashed_to_raw_dii_mapping(hashed_normalized_email, email)
21+
self.hashed_normalized_emails.append(hashed_normalized_email)
22+
else: # phone
23+
if already_hashed:
24+
self.hashed_normalized_phones = emails_or_phones
25+
else:
26+
self.hashed_normalized_phones = []
27+
for phone in emails_or_phones:
28+
hashed_normalized_phone = normalize_and_hash_phone(phone)
29+
self._add_hashed_to_raw_dii_mapping(hashed_normalized_phone, phone)
30+
self.hashed_normalized_phones.append(hashed_normalized_phone)
31+
32+
@staticmethod
33+
def from_emails(emails):
34+
return IdentityMapInput(IdentityType.Email, emails, False)
35+
36+
@staticmethod
37+
def from_phones(phones):
38+
return IdentityMapInput(IdentityType.Phone, phones, False)
39+
40+
@staticmethod
41+
def from_hashed_emails(hashed_emails):
42+
return IdentityMapInput(IdentityType.Email, hashed_emails, True)
43+
44+
@staticmethod
45+
def from_hashed_phones(hashed_phones):
46+
return IdentityMapInput(IdentityType.Phone, hashed_phones, True)
47+
48+
def _add_hashed_to_raw_dii_mapping(self, hashed_dii, raw_dii):
49+
self.hashed_dii_to_raw_diis.setdefault(hashed_dii, []).append(raw_dii)
50+
51+
def get_raw_diis(self, identifier):
52+
if len(self.hashed_dii_to_raw_diis) <= 0:
53+
return [identifier]
54+
else:
55+
return self.hashed_dii_to_raw_diis[identifier]
56+
57+
def get_identity_map_input_as_json_string(self):
58+
json_object = {
59+
"email_hash": self.hashed_normalized_emails,
60+
"phone_hash": self.hashed_normalized_phones
61+
}
62+
return json.dumps({k: v for k, v in json_object.items() if v is not None})

0 commit comments

Comments
 (0)