diff --git a/pyproject.toml b/pyproject.toml index 612bd07..f9e5d00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,9 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -requires-python = ">=3.6" +requires-python = ">=3.8" dependencies = [ - "setuptools", + "requests", "pycryptodome", "bitarray" ] diff --git a/tests/test_identity_map_client.py b/tests/test_identity_map_client.py index 9cdd850..ec8b6c8 100644 --- a/tests/test_identity_map_client.py +++ b/tests/test_identity_map_client.py @@ -2,7 +2,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 @@ -142,22 +142,20 @@ 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()) + self.assertRaises(requests.exceptions.ConnectionError, client.generate_identity_map, identity_map_input) 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()) + self.assertRaises(requests.exceptions.HTTPError, client.generate_identity_map,identity_map_input) 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, + self.assertRaises(requests.exceptions.HTTPError, client.generate_identity_map, identity_map_input) self.assertRaises(HTTPError, client.get_identity_buckets, dt.datetime.now()) diff --git a/tests/test_publisher_client.py b/tests/test_publisher_client.py index 6b06f1b..5afe176 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 @unittest.skipIf( @@ -187,7 +187,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): @@ -197,15 +197,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/__init__.py b/uid2_client/__init__.py index 608a265..06cc20e 100644 --- a/uid2_client/__init__.py +++ b/uid2_client/__init__.py @@ -7,6 +7,7 @@ decrypt_token: decrypt and advertising token to extract advertising ID from it """ +default_new_session = lambda: None from .auto_refresh import * from .client import * diff --git a/uid2_client/identity_map_client.py b/uid2_client/identity_map_client.py index d050456..db373fa 100644 --- a/uid2_client/identity_map_client.py +++ b/uid2_client/identity_map_client.py @@ -1,6 +1,7 @@ import base64 import datetime as dt import json +import time from datetime import timezone from .identity_buckets_response import IdentityBucketsResponse @@ -37,9 +38,13 @@ def __init__(self, base_url, api_key, client_secret): 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()) + start_time = time.time() 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) + resp.raise_for_status() + resp_body = parse_v2_response(self._client_secret, resp.text, nonce) + end_time = time.time() + elapsed_time = end_time - start_time + return IdentityMapResponse(resp_body, identity_map_input, elapsed_time) def get_identity_buckets(self, since_timestamp): req, nonce = make_v2_request(self._client_secret, dt.datetime.now(tz=timezone.utc), diff --git a/uid2_client/identity_map_response.py b/uid2_client/identity_map_response.py index 814cd74..d6c6da0 100644 --- a/uid2_client/identity_map_response.py +++ b/uid2_client/identity_map_response.py @@ -2,12 +2,17 @@ class IdentityMapResponse: - def __init__(self, response, identity_map_input): + def __init__(self, response, identity_map_input, elapsed_time=None): self._mapped_identities = {} self._unmapped_identities = {} + self._response = response + self._elapsed_time = None response_json = json.loads(response) self._status = response_json["status"] + if elapsed_time is not None: + self._elapsed_time = elapsed_time + if not self.is_success(): raise ValueError("Got unexpected identity map status: " + self._status) @@ -44,6 +49,14 @@ def unmapped_identities(self): @property def status(self): return self._status + + @property + def elapsed_time(self): + return self._elapsed_time + + @property + def response(self): + return self._response class MappedIdentity: 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 0caddc3..eb121d7 100644 --- a/uid2_client/request_response_util.py +++ b/uid2_client/request_response_util.py @@ -1,10 +1,12 @@ import base64 +import importlib.metadata import os -from urllib import request +import threading +from typing import Optional +import requests -import pkg_resources - -from uid2_client.encryption import _encrypt_gcm, _decrypt_gcm +import uid2_client +from uid2_client.encryption import _decrypt_gcm, _encrypt_gcm def _make_url(base_url, path): @@ -13,12 +15,12 @@ def _make_url(base_url, path): def auth_headers(auth_key): try: - version = pkg_resources.get_distribution("uid2_client").version + client_version = importlib.metadata.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): @@ -41,6 +43,15 @@ def parse_v2_response(secret_key, encrypted, nonce): return payload[16:] -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) +def __default_new_session(threadlocal=threading.local()): + if getattr(threadlocal, 'session', None) is None: + threadlocal.session = requests.Session() + + return threadlocal.session + + +def post(base_url, path, headers, data, session: Optional[requests.Session] = None): + session = (session or uid2_client.default_new_session() + ) or __default_new_session() + + return session.post(_make_url(base_url, path), data=data, headers=headers, timeout=5)