Skip to content

Commit

Permalink
Merge pull request #46 from IABTechLab/ccm-UID2-3497-implement-identi…
Browse files Browse the repository at this point in the history
…ty-buckets

UID2-3497: Support for /identity/buckets
  • Loading branch information
caroline-ttd authored Jul 4, 2024
2 parents 7f3a72b + 260fd8c commit c815550
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# or the reason why it is unmapped

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

Expand Down
34 changes: 34 additions & 0 deletions examples/sample_get_identity_buckets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import sys
from datetime import datetime

from uid2_client import IdentityMapClient


# this sample client takes timestamp string as input and generates an IdentityBucketsResponse object which contains
# a list of buckets, the timestamp string in the format YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]],
# for example: UTC: 2024-07-02, 2024-07-02T14:30:15.123456+00:00 and EST: 2024-07-02T14:30:15.123456-05:00

def _usage():
print('Usage: python3 sample_get_identity_buckets.py <base_url> <api_key> <client_secret> <timestamp>'
, file=sys.stderr)
sys.exit(1)


if len(sys.argv) <= 4:
_usage()

base_url = sys.argv[1]
api_key = sys.argv[2]
client_secret = sys.argv[3]
timestamp = sys.argv[4]

client = IdentityMapClient(base_url, api_key, client_secret)

identity_buckets_response = client.get_identity_buckets(datetime.fromisoformat(timestamp))

if identity_buckets_response.buckets:
for bucket in identity_buckets_response.buckets:
print("The bucket id of the bucket: ", bucket.get_bucket_id())
print("The last updated timestamp of the bucket: ", bucket.get_last_updated())
else:
print("No bucket was returned")
22 changes: 19 additions & 3 deletions tests/test_identity_map_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import datetime as dt
import os
import unittest

from urllib.error import URLError, HTTPError

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

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

def test_identity_map_bad_url(self):
def test_identity_map_client_bad_url(self):
identity_map_input = IdentityMapInput.from_emails(
["[email protected]", "[email protected]", "[email protected]"])
client = IdentityMapClient("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"), os.getenv("UID2_SECRET_KEY"))
self.assertRaises(URLError, client.generate_identity_map, identity_map_input)
self.assertRaises(URLError, client.get_identity_buckets, dt.datetime.now())

def test_identity_map_bad_api_key(self):
def test_identity_map_client_bad_api_key(self):
identity_map_input = IdentityMapInput.from_emails(
["[email protected]", "[email protected]", "[email protected]"])
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), "bad-api-key", os.getenv("UID2_SECRET_KEY"))
self.assertRaises(HTTPError, client.generate_identity_map,identity_map_input)
self.assertRaises(HTTPError, client.get_identity_buckets, dt.datetime.now())

def test_identity_map_bad_secret(self):
def test_identity_map_client_bad_secret(self):
identity_map_input = IdentityMapInput.from_emails(
["[email protected]", "[email protected]", "[email protected]"])

client = IdentityMapClient(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
self.assertRaises(HTTPError, client.generate_identity_map,
identity_map_input)
self.assertRaises(HTTPError, client.get_identity_buckets,
dt.datetime.now())

def assert_mapped(self, response, dii):
mapped_identity = response.mapped_identities.get(dii)
Expand All @@ -165,6 +172,15 @@ def assert_unmapped(self, response, reason, dii):
mapped_identity = response.mapped_identities.get(dii)
self.assertIsNone(mapped_identity)

def test_identity_buckets(self):
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() - dt.timedelta(days=90))
self.assertTrue(len(response.buckets) > 0)
self.assertTrue(response.is_success)

def test_identity_buckets_empty_response(self):
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() + dt.timedelta(days=1))
self.assertTrue(len(response.buckets) == 0)
self.assertTrue(response.is_success)

if __name__ == '__main__':
unittest.main()
32 changes: 32 additions & 0 deletions tests/test_identity_map_client_unit_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import unittest
import datetime as dt

from uid2_client import IdentityMapClient, get_datetime_utc_iso_format


class IdentityMapUnitTests(unittest.TestCase):
identity_map_client = IdentityMapClient("UID2_BASE_URL", "UID2_API_KEY", "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")

def test_identity_buckets_invalid_timestamp(self):
test_cases = ["1234567890",
1234567890,
2024.7,
"2024-7-1",
"2024-07-01T12:00:00",
[2024, 7, 1, 12, 0, 0],
None]
for timestamp in test_cases:
self.assertRaises(AttributeError, self.identity_map_client.get_identity_buckets,
timestamp)

def test_get_datetime_utc_iso_format_timestamp(self):
expected_timestamp = "2024-07-02T14:30:15.123456"
test_cases = ["2024-07-02T14:30:15.123456+00:00", "2024-07-02 09:30:15.123456-05:00",
"2024-07-02T08:30:15.123456-06:00", "2024-07-02T10:30:15.123456-04:00",
"2024-07-02T06:30:15.123456-08:00", "2024-07-02T23:30:15.123456+09:00",
"2024-07-03T00:30:15.123456+10:00", "2024-07-02T20:00:15.123456+05:30"]
for timestamp_str in test_cases:
timestamp = dt.datetime.fromisoformat(timestamp_str)
iso_format_timestamp = get_datetime_utc_iso_format(timestamp)
self.assertEqual(expected_timestamp, iso_format_timestamp)

46 changes: 46 additions & 0 deletions uid2_client/identity_buckets_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json


class IdentityBucketsResponse:
def __init__(self, response):
self._buckets = []
response_json = json.loads(response)
self._status = response_json["status"]

if not self.is_success():
raise ValueError("Got unexpected identity buckets status: " + self._status)

body = response_json["body"]

for bucket in body:
self._buckets.append(Bucket.from_json(bucket))

def is_success(self):
return self._status == "success"

@property
def buckets(self):
return self._buckets

@property
def status(self):
return self._status


class Bucket:
def __init__(self, bucket_id, last_updated):
self._bucket_id = bucket_id
self._last_updated = last_updated

def get_bucket_id(self):
return self._bucket_id

def get_last_updated(self):
return self._last_updated

@staticmethod
def from_json(json_obj):
return Bucket(
json_obj.get("bucket_id"),
json_obj.get("last_updated")
)
11 changes: 10 additions & 1 deletion uid2_client/identity_map_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import base64
import datetime as dt
import json
from datetime import timezone

from .identity_buckets_response import IdentityBucketsResponse
from .identity_map_response import IdentityMapResponse

from uid2_client import auth_headers, make_v2_request, post, parse_v2_response
from uid2_client import auth_headers, make_v2_request, post, parse_v2_response, get_datetime_utc_iso_format


class IdentityMapClient:
Expand Down Expand Up @@ -38,3 +40,10 @@ def generate_identity_map(self, identity_map_input):
resp = post(self._base_url, '/v2/identity/map', headers=auth_headers(self._api_key), data=req)
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
return IdentityMapResponse(resp_body, identity_map_input)

def get_identity_buckets(self, since_timestamp):
req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc),
json.dumps({"since_timestamp": get_datetime_utc_iso_format(since_timestamp)}).encode())
resp = post(self._base_url, '/v2/identity/buckets', headers=auth_headers(self._api_key), data=req)
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
return IdentityBucketsResponse(resp_body)
7 changes: 7 additions & 0 deletions uid2_client/input_util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import hashlib
import base64
from datetime import timezone


def is_phone_number_normalized(phone_number):
Expand Down Expand Up @@ -119,3 +120,9 @@ def normalize_and_hash_phone(phone):
if not is_phone_number_normalized(phone):
raise ValueError("phone number is not normalized: " + phone)
return get_base64_encoded_hash(phone)


def get_datetime_utc_iso_format(timestamp):
dt_utc = timestamp.astimezone(timezone.utc)
dt_utc_without_tz = dt_utc.replace(tzinfo=None)
return dt_utc_without_tz.isoformat()

0 comments on commit c815550

Please sign in to comment.