Skip to content

Commit db5548d

Browse files
committed
Merge branch 'main' into ccm-UID2-3497-implement-identity-buckets
2 parents 100ec8a + 7f3a72b commit db5548d

9 files changed

+63
-67
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "uid2_client"
10-
version = "3.0.0a1"
10+
version = "2.4.6"
1111
authors = [
1212
{ name = "UID2 team", email = "unifiedid-admin@thetradedesk.com" }
1313
]
@@ -18,9 +18,9 @@ classifiers = [
1818
"License :: OSI Approved :: MIT License",
1919
"Operating System :: OS Independent",
2020
]
21-
requires-python = ">=3.8"
21+
requires-python = ">=3.6"
2222
dependencies = [
23-
"requests",
23+
"setuptools",
2424
"pycryptodome",
2525
"bitarray"
2626
]

tests/test_identity_map_client.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import datetime
1+
import datetime as dt
22
import os
33
import unittest
4-
from datetime import datetime
54

6-
import requests
5+
from urllib.error import URLError, HTTPError
76

87
from uid2_client import IdentityMapClient, IdentityMapInput, normalize_and_hash_email, normalize_and_hash_phone, \
98
get_datetime_utc_iso_format
@@ -137,27 +136,26 @@ def test_identity_map_hashed_phones(self):
137136
def test_identity_map_client_bad_url(self):
138137
identity_map_input = IdentityMapInput.from_emails(
139138
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
140-
client = IdentityMapClient("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"),
141-
os.getenv("UID2_SECRET_KEY"))
142-
self.assertRaises(requests.exceptions.ConnectionError, client.generate_identity_map, identity_map_input)
143-
self.assertRaises(requests.exceptions.ConnectionError, client.get_identity_buckets, datetime.datetime.now())
139+
client = IdentityMapClient("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"), os.getenv("UID2_SECRET_KEY"))
140+
self.assertRaises(URLError, client.generate_identity_map, identity_map_input)
141+
self.assertRaises(URLError, client.get_identity_buckets, dt.datetime.now())
144142

145143
def test_identity_map_client_bad_api_key(self):
146144
identity_map_input = IdentityMapInput.from_emails(
147145
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
148146
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), "bad-api-key", os.getenv("UID2_SECRET_KEY"))
149-
self.assertRaises(requests.exceptions.HTTPError, client.generate_identity_map, identity_map_input)
150-
self.assertRaises(requests.exceptions.HTTPError, client.get_identity_buckets, datetime.datetime.now())
147+
self.assertRaises(HTTPError, client.generate_identity_map,identity_map_input)
148+
self.assertRaises(HTTPError, client.get_identity_buckets, dt.datetime.now())
151149

152150
def test_identity_map_client_bad_secret(self):
153151
identity_map_input = IdentityMapInput.from_emails(
154152
["hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com"])
155-
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"),
156-
"wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
157-
self.assertRaises(requests.exceptions.HTTPError, client.generate_identity_map,
153+
154+
client = IdentityMapClient(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
155+
self.assertRaises(HTTPError, client.generate_identity_map,
158156
identity_map_input)
159-
self.assertRaises(requests.exceptions.HTTPError, client.get_identity_buckets,
160-
datetime.datetime.now())
157+
self.assertRaises(HTTPError, client.get_identity_buckets,
158+
dt.datetime.now())
161159

162160
def assert_mapped(self, response, dii):
163161
mapped_identity = response.mapped_identities.get(dii)
@@ -176,12 +174,12 @@ def assert_unmapped(self, response, reason, dii):
176174
self.assertIsNone(mapped_identity)
177175

178176
def test_identity_buckets(self):
179-
response = self.identity_map_client.get_identity_buckets(datetime.datetime.now() - datetime.timedelta(days=90))
177+
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() - dt.timedelta(days=90))
180178
self.assertTrue(len(response.buckets) > 0)
181179
self.assertTrue(response.is_success)
182180

183181
def test_identity_buckets_empty_response(self):
184-
response = self.identity_map_client.get_identity_buckets(datetime.datetime.now() + datetime.timedelta(days=1))
182+
response = self.identity_map_client.get_identity_buckets(dt.datetime.now() + dt.timedelta(days=1))
185183
self.assertTrue(len(response.buckets) == 0)
186184
self.assertTrue(response.is_success)
187185

@@ -204,7 +202,7 @@ def test_get_datetime_utc_iso_format_timestamp(self):
204202
"2024-07-02T06:30:15.123456-08:00", "2024-07-02T23:30:15.123456+09:00",
205203
"2024-07-03T00:30:15.123456+10:00", "2024-07-02T20:00:15.123456+05:30"]
206204
for timestamp_str in test_cases:
207-
timestamp = datetime.fromisoformat(timestamp_str)
205+
timestamp = dt.datetime.fromisoformat(timestamp_str)
208206
iso_format_timestamp = get_datetime_utc_iso_format(timestamp)
209207
self.assertEqual(expected_timestamp, iso_format_timestamp)
210208

tests/test_publisher_client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import os
22
import unittest
33

4-
import requests
5-
64
from uid2_client import Uid2PublisherClient
75
from uid2_client import TokenGenerateInput
6+
from uid2_client import TokenGenerateResponse
87
from uid2_client.identity_tokens import IdentityTokens
8+
from urllib.request import HTTPError
99

1010

1111
class PublisherEuidIntegrationTests(unittest.TestCase):
@@ -175,7 +175,7 @@ def test_integration_bad_requests(self):
175175

176176
expired_respose = "{\"advertising_token\":\"AgAAAAN6QZRCFTau+sfOlMMUY2ftElFMq2TCrcu1EAaD9WmEfoT2BWm2ZKz1tumbT00tWLffRDQ/9POXfA0O/Ljszn7FLtG5EzTBM3HYs4f5irkqeEvu38DhVCxUEpI+gZZZkynRap1oYx6AmC/ip3rk+7pmqa3r3saDs1mPRSSTm+Nh6A==\",\"user_token\":\"AgAAAAL6aleYI4BubI5ZXMBshqmMEfCkbCJF4fLeg1sdI0BTLzj9sXsSISjkG0lMC743diC2NVy3ElkbO1lLysd+Lm6alkqevPrcuWDisQ1939YdoH6LqpwBH3FNSE4/xa3Q+94=\",\"refresh_token\":\"AAAAAARomrP3NjjH+8mt5djfTHbmRZXjOMnAN8WpjJoe30AhUCvYksO/xoDSj77GzWv4M99DhnPl2cVco8CZFTcE10nauXI4Barr890ILnH0IIacOei5Zjwh6DycFkoXkAAuHY1zjmxb7niGLfSP2RctWkZdRVGWQv/UW/grw6+paU9bnKEWPzVvLwwdW2NgjDKu+szE6A+b5hkY+I3voKoaz8/kLDmX8ddJGLy/YOh/LIveBspSAvEg+v89OuUCwAqm8L3Rt8PxDzDnt0U4Na+AUawvvfsIhmsn/zMpRRks6GHhIAB/EQUHID8TedU8Hv1WFRsiraG9Dfn1Kc5/uYnDJhEagWc+7RgTGT+U5GqI6+afrAl5091eBLbmvXnXn9ts\",\"identity_expires\":1668059799628,\"refresh_expires\":1668142599628,\"refresh_from\":1668056202628,\"refresh_response_key\":\"P941vVeuyjaDRVnFQ8DPd0AZnW4bPeiJPXER2K9QXcU=\"}"
177177
current_identity = IdentityTokens.from_json_string(expired_respose)
178-
with self.assertRaises(requests.exceptions.HTTPError):
178+
with self.assertRaises(HTTPError):
179179
self.publisher_client.refresh_token(current_identity)
180180

181181
with self.assertRaises(TypeError):
@@ -185,15 +185,15 @@ def test_integration_bad_requests(self):
185185
self.publisher_client.refresh_token(None)
186186

187187
bad_url_client = Uid2PublisherClient("https://www.something.com", self.UID2_API_KEY, self.UID2_SECRET_KEY)
188-
with self.assertRaises(requests.exceptions.HTTPError):
188+
with self.assertRaises(HTTPError):
189189
bad_url_client.generate_token(TokenGenerateInput.from_email("test@example.com"))
190190

191191
bad_secret_client = Uid2PublisherClient(self.UID2_BASE_URL, self.UID2_API_KEY, "badSecretKeypB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
192-
with self.assertRaises(requests.exceptions.HTTPError):
192+
with self.assertRaises(HTTPError):
193193
bad_secret_client.generate_token(TokenGenerateInput.from_email("test@example.com"))
194194

195195
bad_api_client = Uid2PublisherClient(self.UID2_BASE_URL, "not-real-key", self.UID2_SECRET_KEY)
196-
with self.assertRaises(requests.exceptions.HTTPError):
196+
with self.assertRaises(HTTPError):
197197
bad_secret_client.generate_token(TokenGenerateInput.from_email("test@example.com"))
198198

199199

tests/test_refresh_keys_util.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import json
22
import unittest
3-
4-
import responses
3+
from unittest.mock import patch
54

65
from uid2_client import refresh_keys_util
76
from test_utils import *
87
from uid2_client.encryption import _encrypt_gcm, _decrypt_gcm
98

109

1110
class TestRefreshKeysUtil(unittest.TestCase):
11+
class MockPostResponse:
12+
def __init__(self, return_value):
13+
self.return_value = return_value
14+
15+
def read(self):
16+
return base64.b64encode(self.return_value)
17+
1218
def _make_post_response(self, request_data, response_payload):
1319
d = base64.b64decode(request_data)[1:]
1420
d = _decrypt_gcm(d, client_secret_bytes)
@@ -19,11 +25,11 @@ def _make_post_response(self, request_data, response_payload):
1925
payload += response_payload
2026
envelope = _encrypt_gcm(payload, None, client_secret_bytes)
2127

22-
return 200, {}, base64.b64encode(envelope)
28+
return self.MockPostResponse(envelope)
2329

24-
def _get_post_refresh_keys_response(self, request):
30+
def _get_post_refresh_keys_response(self, base_url, path, headers, data):
2531
response_payload = key_set_to_json_for_sharing([master_key, site_key]).encode()
26-
return self._make_post_response(request.body, response_payload)
32+
return self._make_post_response(data, response_payload)
2733

2834
def _validate_master_and_site_key(self, keys):
2935
self.assertEqual(len(keys.values()), 2)
@@ -49,29 +55,23 @@ def _validate_master_and_site_key(self, keys):
4955
self.assertEqual(master_secret, master.secret)
5056
self.assertEqual(1, master.keyset_id)
5157

52-
@responses.activate
53-
def test_refresh_sharing_keys(self):
54-
responses.add_callback(
55-
responses.POST,
56-
"https://base_url/v2/key/sharing",
57-
callback=self._get_post_refresh_keys_response,
58-
)
59-
60-
refresh_response = refresh_keys_util.refresh_sharing_keys("https://base_url", "auth_key", base64.b64decode(client_secret))
58+
@patch('uid2_client.refresh_keys_util.post')
59+
def test_refresh_sharing_keys(self, mock_post):
60+
mock_post.side_effect = self._get_post_refresh_keys_response
61+
refresh_response = refresh_keys_util.refresh_sharing_keys("base_url", "auth_key", base64.b64decode(client_secret))
6162
self.assertTrue(refresh_response.success)
6263
self._validate_master_and_site_key(refresh_response.keys)
64+
mock_post.assert_called_once()
65+
self.assertEqual(mock_post.call_args[0], ('base_url', '/v2/key/sharing'))
6366

64-
@responses.activate
65-
def test_refresh_bidstream_keys(self):
66-
responses.add_callback(
67-
responses.POST,
68-
"https://base_url/v2/key/bidstream",
69-
callback=self._get_post_refresh_keys_response,
70-
)
71-
72-
refresh_response = refresh_keys_util.refresh_bidstream_keys("https://base_url", "auth_key", base64.b64decode(client_secret))
67+
@patch('uid2_client.refresh_keys_util.post')
68+
def test_refresh_bidstream_keys(self, mock_post):
69+
mock_post.side_effect = self._get_post_refresh_keys_response
70+
refresh_response = refresh_keys_util.refresh_bidstream_keys("base_url", "auth_key", base64.b64decode(client_secret))
7371
self.assertTrue(refresh_response.success)
7472
self._validate_master_and_site_key(refresh_response.keys)
73+
mock_post.assert_called_once()
74+
self.assertEqual(mock_post.call_args[0], ('base_url', '/v2/key/bidstream'))
7575

7676
def test_parse_keys_json_identity(self):
7777
response_body_str = key_set_to_json_for_sharing([master_key, site_key])

uid2_client/identity_map_client.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@ def generate_identity_map(self, identity_map_input):
3838
req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc),
3939
identity_map_input.get_identity_map_input_as_json_string().encode())
4040
resp = post(self._base_url, '/v2/identity/map', headers=auth_headers(self._api_key), data=req)
41-
resp.raise_for_status()
42-
resp_body = parse_v2_response(self._client_secret, resp.text, nonce)
41+
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
4342
return IdentityMapResponse(resp_body, identity_map_input)
4443

4544
def get_identity_buckets(self, since_timestamp):
4645
req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc),
4746
json.dumps({"since_timestamp": get_datetime_utc_iso_format(since_timestamp)}).encode())
4847
resp = post(self._base_url, '/v2/identity/buckets', headers=auth_headers(self._api_key), data=req)
49-
resp.raise_for_status()
50-
resp_body = parse_v2_response(self._client_secret, resp.text, nonce)
48+
resp_body = parse_v2_response(self._client_secret, resp.read(), nonce)
5149
return IdentityBucketsResponse(resp_body)

uid2_client/publisher_client.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,12 @@ def generate_token(self, token_generate_input):
5050
req, nonce = make_v2_request(self._secret_key, dt.datetime.now(tz=timezone.utc),
5151
token_generate_input.get_as_json_string().encode())
5252
resp = post(self._base_url, '/v2/token/generate', headers=auth_headers(self._auth_key), data=req)
53-
resp.raise_for_status()
54-
resp_body = parse_v2_response(self._secret_key, resp.text, nonce)
53+
resp_body = parse_v2_response(self._secret_key, resp.read(), nonce)
5554
return TokenGenerateResponse(resp_body)
5655

5756
def refresh_token(self, current_identity):
5857
resp = post(self._base_url, '/v2/token/refresh', headers=auth_headers(self._auth_key),
5958
data=current_identity.get_refresh_token().encode())
60-
resp.raise_for_status()
61-
resp_bytes = base64_to_byte_array(resp.text)
59+
resp_bytes = base64_to_byte_array(resp.read())
6260
decrypted = _decrypt_gcm(resp_bytes, base64_to_byte_array(current_identity.get_refresh_response_key()))
6361
return TokenRefreshResponse(decrypted.decode(), dt.datetime.now(tz=timezone.utc))

uid2_client/refresh_keys_util.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def _fetch_keys(base_url, path, auth_key, secret_key):
3838
try:
3939
req, nonce = make_v2_request(secret_key, dt.datetime.now(tz=timezone.utc))
4040
resp = post(base_url, path, headers=auth_headers(auth_key), data=req)
41-
resp.raise_for_status()
42-
resp_body = json.loads(parse_v2_response(secret_key, resp.text, nonce)).get('body')
41+
resp_body = json.loads(parse_v2_response(secret_key, resp.read(), nonce)).get('body')
4342
keys = _parse_keys_json(resp_body)
4443
return RefreshResponse.make_success(keys)
4544
except Exception as exc:

uid2_client/request_response_util.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import base64
2-
from importlib.metadata import version
32
import os
4-
import requests
3+
from urllib import request
4+
5+
import pkg_resources
56

67
from uid2_client.encryption import _encrypt_gcm, _decrypt_gcm
78

@@ -12,12 +13,12 @@ def _make_url(base_url, path):
1213

1314
def auth_headers(auth_key):
1415
try:
15-
client_version = version("uid2_client")
16+
version = pkg_resources.get_distribution("uid2_client").version
1617
except Exception:
17-
client_version = "non-packaged-mode"
18+
version = "non-packaged-mode"
1819

1920
return {'Authorization': 'Bearer ' + auth_key,
20-
"X-UID2-Client-Version": "uid2-client-python-" + client_version}
21+
"X-UID2-Client-Version": "uid2-client-python-" + version}
2122

2223

2324
def make_v2_request(secret_key, now, data=None):
@@ -41,4 +42,5 @@ def parse_v2_response(secret_key, encrypted, nonce):
4142

4243

4344
def post(base_url, path, headers, data):
44-
return requests.post(_make_url(base_url, path), data=data, headers=headers)
45+
req = request.Request(_make_url(base_url, path), headers=headers, method='POST', data=data)
46+
return request.urlopen(req)

uid2_client/uid2_token_generator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class UID2TokenGenerator:
7070

7171
@staticmethod
7272
def generate_uid2_token_v2(id_str, master_key, site_id, site_key, params=None, version=2):
73+
"""This function is only used by tests."""
7374
if params is None:
7475
params = Params()
7576

@@ -80,11 +81,11 @@ def generate_uid2_token_v2(id_str, master_key, site_id, site_key, params=None, v
8081
# old privacy_bits
8182
identity += int.to_bytes(0, 4, 'big')
8283
identity += int.to_bytes(int(params.identity_established.timestamp()) * 1000, 8, 'big')
83-
identity_iv = bytes([10, 11, 12, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9])
84+
identity_iv = os.urandom(16)
8485
expiry = params.token_expiry
8586
master_payload = int.to_bytes(int(expiry.timestamp()) * 1000, 8, 'big')
8687
master_payload += _encrypt_data_v1(identity, key=site_key, iv=identity_iv)
87-
master_iv = bytes([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36])
88+
master_iv = os.urandom(16)
8889

8990
token = int.to_bytes(version, 1, 'big')
9091
token += _encrypt_data_v1(master_payload, key=master_key, iv=master_iv)

0 commit comments

Comments
 (0)