Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 28ab011

Browse files
committedMay 11, 2024·
Add IdentityMapClient
1 parent 252b57e commit 28ab011

File tree

5 files changed

+211
-2
lines changed

5 files changed

+211
-2
lines changed
 
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 or phone numbers as input and generates an IdentityMapResponse object
7+
# which contains raw uid or the reason why it is unmapped
8+
9+
def _usage():
10+
print('Usage: python3 sample_sharing_client.py <base_url> <auth_key> <secret_key> <email_list>', file=sys.stderr)
11+
sys.exit(1)
12+
13+
14+
if len(sys.argv) <= 4:
15+
_usage()
16+
17+
base_url = sys.argv[1]
18+
auth_key = sys.argv[2]
19+
secret_key = sys.argv[3]
20+
email_list = sys.argv[4:]
21+
first_email = sys.argv[4]
22+
23+
client = IdentityMapClient(base_url, auth_key, secret_key)
24+
25+
identity_map_response = client.generate_identity_map(IdentityMapInput.from_emails(email_list))
26+
27+
mapped_identities = identity_map_response.mapped_identities
28+
unmapped_identities = identity_map_response.unmapped_identities
29+
30+
mapped_identity = mapped_identities.get(first_email)
31+
if mapped_identity is not None:
32+
raw_uid = mapped_identity.get_raw_id()
33+
print('raw_uid =', raw_uid)
34+
else:
35+
unmapped_identity = unmapped_identities.get(first_email)
36+
reason = unmapped_identity.get_reason()
37+
print('reason =', reason)
38+

‎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 UID2 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, auth_key, secret_key):
21+
"""Create a new IdentityMapClient client.
22+
23+
Args:
24+
base_url (str): base URL for all requests to UID2 services (e.g. 'https://prod.uidapi.com')
25+
auth_key (str): authorization key for consuming the UID2 services
26+
secret_key (str): secret key for consuming the UID2 services
27+
28+
Note:
29+
Your authorization key will determine which UID2 services you are allowed to use.
30+
"""
31+
self._base_url = base_url
32+
self._auth_key = auth_key
33+
self._secret_key = base64.b64decode(secret_key)
34+
35+
def generate_identity_map(self, identity_map_input):
36+
req, nonce = make_v2_request(self._secret_key, 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._auth_key), data=req)
39+
resp_body = parse_v2_response(self._secret_key, resp.read(), nonce)
40+
return IdentityMapResponse(resp_body, identity_map_input)

‎uid2_client/identity_map_input.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
3+
from uid2_client import IdentityType, normalize_email_string, get_base64_encoded_hash, is_phone_number_normalized
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 = []
12+
self.hashed_normalized_phones = []
13+
if identity_type == IdentityType.Email:
14+
for email in emails_or_phones:
15+
if already_hashed:
16+
self.hashed_normalized_emails.append(email)
17+
else:
18+
normalized_email = normalize_email_string(email)
19+
if normalized_email is None:
20+
raise ValueError("invalid email address")
21+
hashed_normalized_email = get_base64_encoded_hash(normalized_email)
22+
self.hashed_normalized_emails.append(hashed_normalized_email)
23+
self._add_hashed_to_raw_dii_mapping(hashed_normalized_email, email)
24+
else: # phone
25+
for phone in emails_or_phones:
26+
if already_hashed:
27+
self.hashed_normalized_phones.append(phone)
28+
else:
29+
if not is_phone_number_normalized(phone):
30+
raise ValueError("phone number is not normalized: " + phone)
31+
hashed_normalized_phone = get_base64_encoded_hash(phone)
32+
self._add_hashed_to_raw_dii_mapping(hashed_normalized_phone, phone)
33+
self.hashed_normalized_phones.append(hashed_normalized_phone)
34+
35+
@staticmethod
36+
def from_emails(emails):
37+
return IdentityMapInput(IdentityType.Email, emails, False)
38+
39+
@staticmethod
40+
def from_phones(phones):
41+
return IdentityMapInput(IdentityType.Phone, phones, False)
42+
43+
@staticmethod
44+
def from_hashed_emails(hashed_emails):
45+
return IdentityMapInput(IdentityType.Email, hashed_emails, True)
46+
47+
@staticmethod
48+
def from_hashed_phones(hashed_phones):
49+
return IdentityMapInput(IdentityType.Phone, hashed_phones, True)
50+
51+
def _add_hashed_to_raw_dii_mapping(self, hashed_dii, raw_dii):
52+
self.hashed_dii_to_raw_diis.setdefault(hashed_dii, []).append(raw_dii)
53+
54+
def get_identity_map_input_as_json_string(self):
55+
json_object = {
56+
"email_hash": self.hashed_normalized_emails,
57+
"phone_hash": self.hashed_normalized_phones
58+
}
59+
return json.dumps({k: v for k, v in json_object.items() if v is not None and len(v) > 0})

‎uid2_client/identity_map_response.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import json
2+
3+
4+
class IdentityMapResponse:
5+
def __init__(self, response, identity_map_input):
6+
self.mapped_identities = {}
7+
self.unmapped_identities = {}
8+
response_json = json.loads(response)
9+
self.status = response_json["status"]
10+
11+
if not self.is_success():
12+
raise ValueError("Got unexpected identity map status: " + self.status)
13+
14+
body = self._get_body_as_json(response_json)
15+
16+
for identity in body.get("mapped", []):
17+
raw_diis = self._get_raw_diis(identity, identity_map_input)
18+
mapped_identity = MappedIdentity.from_json(identity)
19+
for raw_dii in raw_diis:
20+
self.mapped_identities[raw_dii] = mapped_identity
21+
22+
for identity in body.get("unmapped", []):
23+
raw_diis = self._get_raw_diis(identity, identity_map_input)
24+
unmapped_identity = UnmappedIdentity.from_json(identity)
25+
for raw_dii in raw_diis:
26+
self.unmapped_identities[raw_dii] = unmapped_identity
27+
28+
@staticmethod
29+
def _get_body_as_json(json_response):
30+
return json_response["body"]
31+
32+
@staticmethod
33+
def _get_raw_diis(identity, identity_map_input):
34+
identifier = identity["identifier"]
35+
return identity_map_input.hashed_dii_to_raw_diis[identifier]
36+
37+
def is_success(self):
38+
return self.status == "success"
39+
40+
41+
class MappedIdentity:
42+
def __init__(self, raw_uid, bucket_id):
43+
self.raw_uid = raw_uid
44+
self.bucket_id = bucket_id
45+
46+
def get_raw_id(self):
47+
return self.raw_uid
48+
49+
def get_bucket_id(self):
50+
return self.bucket_id
51+
52+
@staticmethod
53+
def from_json(json_obj):
54+
return MappedIdentity(
55+
json_obj.get("advertising_id"),
56+
json_obj.get("bucket_id")
57+
)
58+
59+
60+
class UnmappedIdentity:
61+
def __init__(self, reason):
62+
self.reason = reason
63+
64+
def get_reason(self):
65+
return self.reason
66+
67+
@staticmethod
68+
def from_json(json_obj):
69+
return UnmappedIdentity(
70+
json_obj.get("reason")
71+
)

0 commit comments

Comments
 (0)
Please sign in to comment.