From 15569c3e3c1e496930dd99584a3d3f1a1d01496b Mon Sep 17 00:00:00 2001 From: Matt Collins Date: Thu, 20 Jun 2024 11:24:45 +1000 Subject: [PATCH 1/3] Update minimum Python version to 3.8, SDK version to 3.0.0a1 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bb92e92..ec67a9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "uid2_client" -version = "2.4.6" +version = "3.0.0a1" authors = [ { name = "UID2 team", email = "unifiedid-admin@thetradedesk.com" } ] @@ -18,7 +18,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -requires-python = ">=3.6" +requires-python = ">=3.8" dependencies = [ "setuptools", "pycryptodome", From 8506445415756dddf815640050120264cb12f3e9 Mon Sep 17 00:00:00 2001 From: Matt Collins Date: Thu, 20 Jun 2024 11:26:04 +1000 Subject: [PATCH 2/3] Remove deprecated pkg_resources in favor of importlib.metadata --- pyproject.toml | 1 - uid2_client/request_response_util.py | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec67a9c..385519d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "setuptools", "pycryptodome", "bitarray" ] diff --git a/uid2_client/request_response_util.py b/uid2_client/request_response_util.py index 0caddc3..0809102 100644 --- a/uid2_client/request_response_util.py +++ b/uid2_client/request_response_util.py @@ -1,9 +1,8 @@ import base64 +from importlib.metadata import version import os from urllib import request -import pkg_resources - from uid2_client.encryption import _encrypt_gcm, _decrypt_gcm @@ -13,12 +12,12 @@ def _make_url(base_url, path): def auth_headers(auth_key): try: - version = pkg_resources.get_distribution("uid2_client").version + client_version = version("uid2_client") except Exception: - version = "non-packaged-mode" + client_version = "non-packaged-mode" return {'Authorization': 'Bearer ' + auth_key, - "X-UID2-Client-Version": "uid2-client-python-" + version} + "X-UID2-Client-Version": "uid2-client-python-" + client_version} def make_v2_request(secret_key, now, data=None): From afebbab202d28dab4a306e05cd39c83e483e97df Mon Sep 17 00:00:00 2001 From: Matt Collins Date: Thu, 20 Jun 2024 10:55:38 +1000 Subject: [PATCH 3/3] Change from urllib to requests --- pyproject.toml | 1 + tests/test_identity_map_client.py | 9 +++--- tests/test_publisher_client.py | 12 ++++---- tests/test_refresh_keys_util.py | 46 ++++++++++++++-------------- uid2_client/identity_map_client.py | 3 +- uid2_client/publisher_client.py | 6 ++-- uid2_client/refresh_keys_util.py | 3 +- uid2_client/request_response_util.py | 5 ++- 8 files changed, 45 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 385519d..4cceb55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ + "requests", "pycryptodome", "bitarray" ] diff --git a/tests/test_identity_map_client.py b/tests/test_identity_map_client.py index 1521be5..56411fa 100644 --- a/tests/test_identity_map_client.py +++ b/tests/test_identity_map_client.py @@ -1,6 +1,7 @@ import os import unittest -from urllib.error import URLError, HTTPError + +import requests from uid2_client import IdentityMapClient, IdentityMapInput, normalize_and_hash_email, normalize_and_hash_phone @@ -134,19 +135,19 @@ def test_identity_map_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(requests.exceptions.ConnectionError, client.generate_identity_map, identity_map_input) def test_identity_map_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(requests.exceptions.HTTPError, client.generate_identity_map,identity_map_input) def test_identity_map_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, + self.assertRaises(requests.exceptions.HTTPError, client.generate_identity_map, identity_map_input) def assert_mapped(self, response, dii): diff --git a/tests/test_publisher_client.py b/tests/test_publisher_client.py index 090e1d0..42f63d6 100644 --- a/tests/test_publisher_client.py +++ b/tests/test_publisher_client.py @@ -1,11 +1,11 @@ import os import unittest +import requests + from uid2_client import Uid2PublisherClient from uid2_client import TokenGenerateInput -from uid2_client import TokenGenerateResponse from uid2_client.identity_tokens import IdentityTokens -from urllib.request import HTTPError class PublisherEuidIntegrationTests(unittest.TestCase): @@ -175,7 +175,7 @@ def test_integration_bad_requests(self): 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=\"}" current_identity = IdentityTokens.from_json_string(expired_respose) - with self.assertRaises(HTTPError): + with self.assertRaises(requests.exceptions.HTTPError): self.publisher_client.refresh_token(current_identity) with self.assertRaises(TypeError): @@ -185,15 +185,15 @@ def test_integration_bad_requests(self): self.publisher_client.refresh_token(None) bad_url_client = Uid2PublisherClient("https://www.something.com", self.UID2_API_KEY, self.UID2_SECRET_KEY) - with self.assertRaises(HTTPError): + with self.assertRaises(requests.exceptions.HTTPError): bad_url_client.generate_token(TokenGenerateInput.from_email("test@example.com")) bad_secret_client = Uid2PublisherClient(self.UID2_BASE_URL, self.UID2_API_KEY, "badSecretKeypB64Y3fV2dAed8t/mupw3sjN5jNRFzg=") - with self.assertRaises(HTTPError): + with self.assertRaises(requests.exceptions.HTTPError): bad_secret_client.generate_token(TokenGenerateInput.from_email("test@example.com")) bad_api_client = Uid2PublisherClient(self.UID2_BASE_URL, "not-real-key", self.UID2_SECRET_KEY) - with self.assertRaises(HTTPError): + with self.assertRaises(requests.exceptions.HTTPError): bad_secret_client.generate_token(TokenGenerateInput.from_email("test@example.com")) diff --git a/tests/test_refresh_keys_util.py b/tests/test_refresh_keys_util.py index b6273db..5b50c9a 100644 --- a/tests/test_refresh_keys_util.py +++ b/tests/test_refresh_keys_util.py @@ -1,6 +1,7 @@ import json import unittest -from unittest.mock import patch + +import responses from uid2_client import refresh_keys_util from test_utils import * @@ -8,13 +9,6 @@ class TestRefreshKeysUtil(unittest.TestCase): - class MockPostResponse: - def __init__(self, return_value): - self.return_value = return_value - - def read(self): - return base64.b64encode(self.return_value) - def _make_post_response(self, request_data, response_payload): d = base64.b64decode(request_data)[1:] d = _decrypt_gcm(d, client_secret_bytes) @@ -25,11 +19,11 @@ def _make_post_response(self, request_data, response_payload): payload += response_payload envelope = _encrypt_gcm(payload, None, client_secret_bytes) - return self.MockPostResponse(envelope) + return 200, {}, base64.b64encode(envelope) - def _get_post_refresh_keys_response(self, base_url, path, headers, data): + def _get_post_refresh_keys_response(self, request): response_payload = key_set_to_json_for_sharing([master_key, site_key]).encode() - return self._make_post_response(data, response_payload) + return self._make_post_response(request.body, response_payload) def _validate_master_and_site_key(self, keys): self.assertEqual(len(keys.values()), 2) @@ -55,23 +49,29 @@ def _validate_master_and_site_key(self, keys): self.assertEqual(master_secret, master.secret) self.assertEqual(1, master.keyset_id) - @patch('uid2_client.refresh_keys_util.post') - def test_refresh_sharing_keys(self, mock_post): - mock_post.side_effect = self._get_post_refresh_keys_response - refresh_response = refresh_keys_util.refresh_sharing_keys("base_url", "auth_key", base64.b64decode(client_secret)) + @responses.activate + def test_refresh_sharing_keys(self): + responses.add_callback( + responses.POST, + "https://base_url/v2/key/sharing", + callback=self._get_post_refresh_keys_response, + ) + + refresh_response = refresh_keys_util.refresh_sharing_keys("https://base_url", "auth_key", base64.b64decode(client_secret)) self.assertTrue(refresh_response.success) self._validate_master_and_site_key(refresh_response.keys) - mock_post.assert_called_once() - self.assertEqual(mock_post.call_args[0], ('base_url', '/v2/key/sharing')) - @patch('uid2_client.refresh_keys_util.post') - def test_refresh_bidstream_keys(self, mock_post): - mock_post.side_effect = self._get_post_refresh_keys_response - refresh_response = refresh_keys_util.refresh_bidstream_keys("base_url", "auth_key", base64.b64decode(client_secret)) + @responses.activate + def test_refresh_bidstream_keys(self): + responses.add_callback( + responses.POST, + "https://base_url/v2/key/bidstream", + callback=self._get_post_refresh_keys_response, + ) + + refresh_response = refresh_keys_util.refresh_bidstream_keys("https://base_url", "auth_key", base64.b64decode(client_secret)) self.assertTrue(refresh_response.success) self._validate_master_and_site_key(refresh_response.keys) - mock_post.assert_called_once() - self.assertEqual(mock_post.call_args[0], ('base_url', '/v2/key/bidstream')) def test_parse_keys_json_identity(self): response_body_str = key_set_to_json_for_sharing([master_key, site_key]) diff --git a/uid2_client/identity_map_client.py b/uid2_client/identity_map_client.py index 6466685..7829a0c 100644 --- a/uid2_client/identity_map_client.py +++ b/uid2_client/identity_map_client.py @@ -36,5 +36,6 @@ def generate_identity_map(self, identity_map_input): req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc), identity_map_input.get_identity_map_input_as_json_string().encode()) 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) + resp.raise_for_status() + resp_body = parse_v2_response(self._client_secret, resp.text, nonce) return IdentityMapResponse(resp_body, identity_map_input) diff --git a/uid2_client/publisher_client.py b/uid2_client/publisher_client.py index 80c4496..c9fa846 100644 --- a/uid2_client/publisher_client.py +++ b/uid2_client/publisher_client.py @@ -50,12 +50,14 @@ def generate_token(self, token_generate_input): req, nonce = make_v2_request(self._secret_key, dt.datetime.now(tz=timezone.utc), token_generate_input.get_as_json_string().encode()) resp = post(self._base_url, '/v2/token/generate', headers=auth_headers(self._auth_key), data=req) - resp_body = parse_v2_response(self._secret_key, resp.read(), nonce) + resp.raise_for_status() + resp_body = parse_v2_response(self._secret_key, resp.text, nonce) return TokenGenerateResponse(resp_body) def refresh_token(self, current_identity): resp = post(self._base_url, '/v2/token/refresh', headers=auth_headers(self._auth_key), data=current_identity.get_refresh_token().encode()) - resp_bytes = base64_to_byte_array(resp.read()) + resp.raise_for_status() + resp_bytes = base64_to_byte_array(resp.text) decrypted = _decrypt_gcm(resp_bytes, base64_to_byte_array(current_identity.get_refresh_response_key())) return TokenRefreshResponse(decrypted.decode(), dt.datetime.now(tz=timezone.utc)) diff --git a/uid2_client/refresh_keys_util.py b/uid2_client/refresh_keys_util.py index 791d89b..f27eae0 100644 --- a/uid2_client/refresh_keys_util.py +++ b/uid2_client/refresh_keys_util.py @@ -38,7 +38,8 @@ def _fetch_keys(base_url, path, auth_key, secret_key): try: req, nonce = make_v2_request(secret_key, dt.datetime.now(tz=timezone.utc)) resp = post(base_url, path, headers=auth_headers(auth_key), data=req) - resp_body = json.loads(parse_v2_response(secret_key, resp.read(), nonce)).get('body') + resp.raise_for_status() + resp_body = json.loads(parse_v2_response(secret_key, resp.text, nonce)).get('body') keys = _parse_keys_json(resp_body) return RefreshResponse.make_success(keys) except Exception as exc: diff --git a/uid2_client/request_response_util.py b/uid2_client/request_response_util.py index 0809102..c4741b7 100644 --- a/uid2_client/request_response_util.py +++ b/uid2_client/request_response_util.py @@ -1,7 +1,7 @@ import base64 from importlib.metadata import version import os -from urllib import request +import requests from uid2_client.encryption import _encrypt_gcm, _decrypt_gcm @@ -41,5 +41,4 @@ def parse_v2_response(secret_key, encrypted, nonce): def post(base_url, path, headers, data): - req = request.Request(_make_url(base_url, path), headers=headers, method='POST', data=data) - return request.urlopen(req) + return requests.post(_make_url(base_url, path), data=data, headers=headers)