diff --git a/examples/sample_identity_map_client.py b/examples/sample_generate_identity_map.py similarity index 88% rename from examples/sample_identity_map_client.py rename to examples/sample_generate_identity_map.py index a0dad6d..acb2483 100644 --- a/examples/sample_identity_map_client.py +++ b/examples/sample_generate_identity_map.py @@ -7,7 +7,7 @@ # or the reason why it is unmapped def _usage(): - print('Usage: python3 sample_identity_map_client.py ... ' + print('Usage: python3 sample_generate_identity_map.py ... ' , file=sys.stderr) sys.exit(1) diff --git a/examples/sample_get_identity_buckets.py b/examples/sample_get_identity_buckets.py new file mode 100644 index 0000000..1d42820 --- /dev/null +++ b/examples/sample_get_identity_buckets.py @@ -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 ' + , 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") diff --git a/tests/test_identity_map_client.py b/tests/test_identity_map_client.py index 1521be5..f59b77d 100644 --- a/tests/test_identity_map_client.py +++ b/tests/test_identity_map_client.py @@ -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 @@ -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( ["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"]) 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( ["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"]) 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( ["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"]) + 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) @@ -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() diff --git a/tests/test_identity_map_client_unit_tests.py b/tests/test_identity_map_client_unit_tests.py new file mode 100644 index 0000000..7a928e3 --- /dev/null +++ b/tests/test_identity_map_client_unit_tests.py @@ -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) + diff --git a/uid2_client/identity_buckets_response.py b/uid2_client/identity_buckets_response.py new file mode 100644 index 0000000..8e0ccf2 --- /dev/null +++ b/uid2_client/identity_buckets_response.py @@ -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") + ) diff --git a/uid2_client/identity_map_client.py b/uid2_client/identity_map_client.py index 6466685..d050456 100644 --- a/uid2_client/identity_map_client.py +++ b/uid2_client/identity_map_client.py @@ -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: @@ -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) diff --git a/uid2_client/input_util.py b/uid2_client/input_util.py index 6d85f63..dcf6013 100644 --- a/uid2_client/input_util.py +++ b/uid2_client/input_util.py @@ -1,5 +1,6 @@ import hashlib import base64 +from datetime import timezone def is_phone_number_normalized(phone_number): @@ -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()