Skip to content

Commit c815550

Browse files
authored
Merge pull request #46 from IABTechLab/ccm-UID2-3497-implement-identity-buckets
UID2-3497: Support for /identity/buckets
2 parents 7f3a72b + 260fd8c commit c815550

7 files changed

+149
-5
lines changed

examples/sample_identity_map_client.py renamed to examples/sample_generate_identity_map.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# or the reason why it is unmapped
88

99
def _usage():
10-
print('Usage: python3 sample_identity_map_client.py <base_url> <api_key> <client_secret> <email_1> <email_2> ... <email_n>'
10+
print('Usage: python3 sample_generate_identity_map.py <base_url> <api_key> <client_secret> <email_1> <email_2> ... <email_n>'
1111
, file=sys.stderr)
1212
sys.exit(1)
1313

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import sys
2+
from datetime import datetime
3+
4+
from uid2_client import IdentityMapClient
5+
6+
7+
# this sample client takes timestamp string as input and generates an IdentityBucketsResponse object which contains
8+
# a list of buckets, the timestamp string in the format YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]],
9+
# for example: UTC: 2024-07-02, 2024-07-02T14:30:15.123456+00:00 and EST: 2024-07-02T14:30:15.123456-05:00
10+
11+
def _usage():
12+
print('Usage: python3 sample_get_identity_buckets.py <base_url> <api_key> <client_secret> <timestamp>'
13+
, file=sys.stderr)
14+
sys.exit(1)
15+
16+
17+
if len(sys.argv) <= 4:
18+
_usage()
19+
20+
base_url = sys.argv[1]
21+
api_key = sys.argv[2]
22+
client_secret = sys.argv[3]
23+
timestamp = sys.argv[4]
24+
25+
client = IdentityMapClient(base_url, api_key, client_secret)
26+
27+
identity_buckets_response = client.get_identity_buckets(datetime.fromisoformat(timestamp))
28+
29+
if identity_buckets_response.buckets:
30+
for bucket in identity_buckets_response.buckets:
31+
print("The bucket id of the bucket: ", bucket.get_bucket_id())
32+
print("The last updated timestamp of the bucket: ", bucket.get_last_updated())
33+
else:
34+
print("No bucket was returned")

tests/test_identity_map_client.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import datetime as dt
12
import os
23
import unittest
4+
35
from urllib.error import URLError, HTTPError
46

57
from uid2_client import IdentityMapClient, IdentityMapInput, normalize_and_hash_email, normalize_and_hash_phone
@@ -130,24 +132,29 @@ def test_identity_map_hashed_phones(self):
130132

131133
self.assert_unmapped(response, "optout", hashed_opted_out_phone)
132134

133-
def test_identity_map_bad_url(self):
135+
def test_identity_map_client_bad_url(self):
134136
identity_map_input = IdentityMapInput.from_emails(
135137
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
136138
client = IdentityMapClient("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"), os.getenv("UID2_SECRET_KEY"))
137139
self.assertRaises(URLError, client.generate_identity_map, identity_map_input)
140+
self.assertRaises(URLError, client.get_identity_buckets, dt.datetime.now())
138141

139-
def test_identity_map_bad_api_key(self):
142+
def test_identity_map_client_bad_api_key(self):
140143
identity_map_input = IdentityMapInput.from_emails(
141144
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
142145
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), "bad-api-key", os.getenv("UID2_SECRET_KEY"))
143146
self.assertRaises(HTTPError, client.generate_identity_map,identity_map_input)
147+
self.assertRaises(HTTPError, client.get_identity_buckets, dt.datetime.now())
144148

145-
def test_identity_map_bad_secret(self):
149+
def test_identity_map_client_bad_secret(self):
146150
identity_map_input = IdentityMapInput.from_emails(
147151
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
152+
148153
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
149154
self.assertRaises(HTTPError, client.generate_identity_map,
150155
identity_map_input)
156+
self.assertRaises(HTTPError, client.get_identity_buckets,
157+
dt.datetime.now())
151158

152159
def assert_mapped(self, response, dii):
153160
mapped_identity = response.mapped_identities.get(dii)
@@ -165,6 +172,15 @@ def assert_unmapped(self, response, reason, dii):
165172
mapped_identity = response.mapped_identities.get(dii)
166173
self.assertIsNone(mapped_identity)
167174

175+
def test_identity_buckets(self):
176+
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() - dt.timedelta(days=90))
177+
self.assertTrue(len(response.buckets) > 0)
178+
self.assertTrue(response.is_success)
179+
180+
def test_identity_buckets_empty_response(self):
181+
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() + dt.timedelta(days=1))
182+
self.assertTrue(len(response.buckets) == 0)
183+
self.assertTrue(response.is_success)
168184

169185
if __name__ == '__main__':
170186
unittest.main()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
import datetime as dt
3+
4+
from uid2_client import IdentityMapClient, get_datetime_utc_iso_format
5+
6+
7+
class IdentityMapUnitTests(unittest.TestCase):
8+
identity_map_client = IdentityMapClient("UID2_BASE_URL", "UID2_API_KEY", "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
9+
10+
def test_identity_buckets_invalid_timestamp(self):
11+
test_cases = ["1234567890",
12+
1234567890,
13+
2024.7,
14+
"2024-7-1",
15+
"2024-07-01T12:00:00",
16+
[2024, 7, 1, 12, 0, 0],
17+
None]
18+
for timestamp in test_cases:
19+
self.assertRaises(AttributeError, self.identity_map_client.get_identity_buckets,
20+
timestamp)
21+
22+
def test_get_datetime_utc_iso_format_timestamp(self):
23+
expected_timestamp = "2024-07-02T14:30:15.123456"
24+
test_cases = ["2024-07-02T14:30:15.123456+00:00", "2024-07-02 09:30:15.123456-05:00",
25+
"2024-07-02T08:30:15.123456-06:00", "2024-07-02T10:30:15.123456-04:00",
26+
"2024-07-02T06:30:15.123456-08:00", "2024-07-02T23:30:15.123456+09:00",
27+
"2024-07-03T00:30:15.123456+10:00", "2024-07-02T20:00:15.123456+05:30"]
28+
for timestamp_str in test_cases:
29+
timestamp = dt.datetime.fromisoformat(timestamp_str)
30+
iso_format_timestamp = get_datetime_utc_iso_format(timestamp)
31+
self.assertEqual(expected_timestamp, iso_format_timestamp)
32+
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import json
2+
3+
4+
class IdentityBucketsResponse:
5+
def __init__(self, response):
6+
self._buckets = []
7+
response_json = json.loads(response)
8+
self._status = response_json["status"]
9+
10+
if not self.is_success():
11+
raise ValueError("Got unexpected identity buckets status: " + self._status)
12+
13+
body = response_json["body"]
14+
15+
for bucket in body:
16+
self._buckets.append(Bucket.from_json(bucket))
17+
18+
def is_success(self):
19+
return self._status == "success"
20+
21+
@property
22+
def buckets(self):
23+
return self._buckets
24+
25+
@property
26+
def status(self):
27+
return self._status
28+
29+
30+
class Bucket:
31+
def __init__(self, bucket_id, last_updated):
32+
self._bucket_id = bucket_id
33+
self._last_updated = last_updated
34+
35+
def get_bucket_id(self):
36+
return self._bucket_id
37+
38+
def get_last_updated(self):
39+
return self._last_updated
40+
41+
@staticmethod
42+
def from_json(json_obj):
43+
return Bucket(
44+
json_obj.get("bucket_id"),
45+
json_obj.get("last_updated")
46+
)

uid2_client/identity_map_client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import base64
22
import datetime as dt
3+
import json
34
from datetime import timezone
45

6+
from .identity_buckets_response import IdentityBucketsResponse
57
from .identity_map_response import IdentityMapResponse
68

7-
from uid2_client import auth_headers, make_v2_request, post, parse_v2_response
9+
from uid2_client import auth_headers, make_v2_request, post, parse_v2_response, get_datetime_utc_iso_format
810

911

1012
class IdentityMapClient:
@@ -38,3 +40,10 @@ def generate_identity_map(self, identity_map_input):
3840
resp = post(self._base_url, '/v2/identity/map', headers=auth_headers(self._api_key), data=req)
3941
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
4042
return IdentityMapResponse(resp_body, identity_map_input)
43+
44+
def get_identity_buckets(self, since_timestamp):
45+
req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc),
46+
json.dumps({"since_timestamp": get_datetime_utc_iso_format(since_timestamp)}).encode())
47+
resp = post(self._base_url, '/v2/identity/buckets', headers=auth_headers(self._api_key), data=req)
48+
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
49+
return IdentityBucketsResponse(resp_body)

uid2_client/input_util.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import hashlib
22
import base64
3+
from datetime import timezone
34

45

56
def is_phone_number_normalized(phone_number):
@@ -119,3 +120,9 @@ def normalize_and_hash_phone(phone):
119120
if not is_phone_number_normalized(phone):
120121
raise ValueError("phone number is not normalized: " + phone)
121122
return get_base64_encoded_hash(phone)
123+
124+
125+
def get_datetime_utc_iso_format(timestamp):
126+
dt_utc = timestamp.astimezone(timezone.utc)
127+
dt_utc_without_tz = dt_utc.replace(tzinfo=None)
128+
return dt_utc_without_tz.isoformat()

0 commit comments

Comments
 (0)