Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UID2-3497: Support for /identity/buckets #46

Merged
merged 10 commits into from
Jul 4, 2024
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):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked how Python's @patch and call_args work together. Creating a single simple method might be easier for this unit tests.

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