From f623b450313a06e6e6c9bca144478bcd84e481d2 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 14:43:46 -0600 Subject: [PATCH 01/14] Update code to retry web API calls for fetching datafile and pushing events --- optimizely/config_manager.py | 32 ++++++++++++++++++++++++++------ optimizely/event_dispatcher.py | 16 ++++++++++++++-- optimizely/helpers/enums.py | 1 + 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index c959914e..8c3838f3 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -19,6 +19,8 @@ import threading from requests import codes as http_status_codes from requests import exceptions as requests_exceptions +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from . import exceptions as optimizely_exceptions from . import logger as optimizely_logger @@ -200,6 +202,7 @@ def __init__( error_handler: Optional[BaseErrorHandler] = None, notification_center: Optional[NotificationCenter] = None, skip_json_validation: Optional[bool] = False, + retries: Optional[int] = 3, ): """ Initialize config manager. One of sdk_key or datafile has to be set to be able to use. @@ -244,6 +247,7 @@ def __init__( self.stopped = threading.Event() self._initialize_thread() self._polling_thread.start() + self.retries = retries @staticmethod def get_datafile_url(sdk_key: Optional[str], url: Optional[str], url_template: Optional[str]) -> str: @@ -391,9 +395,17 @@ def fetch_datafile(self) -> None: request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified try: - response = requests.get( - self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT, - ) + print(f"Getting {self.datafile_url}") + session = requests.Session() + + retries = Retry(total=self.retries, + backoff_factor=0.1, + status_forcelist=[ 500, 502, 503, 504 ]) + adapter = HTTPAdapter(max_retries=retries) + + session.mount('http://', adapter) + session.mount("https://", adapter) + response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return @@ -475,9 +487,17 @@ def fetch_datafile(self) -> None: request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified try: - response = requests.get( - self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT, - ) + print(f"Getting {self.datafile_url}") + session = requests.Session() + + retries = Retry(total=self.retries, + backoff_factor=0.1, + status_forcelist=[ 500, 502, 503, 504 ]) + adapter = HTTPAdapter(max_retries=retries) + + session.mount('http://', adapter) + session.mount("https://", adapter) + response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return diff --git a/optimizely/event_dispatcher.py b/optimizely/event_dispatcher.py index e2ca54f0..94b01072 100644 --- a/optimizely/event_dispatcher.py +++ b/optimizely/event_dispatcher.py @@ -17,6 +17,8 @@ import requests from requests import exceptions as request_exception +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from . import event_builder from .helpers.enums import HTTPVerbs, EventDispatchConfig @@ -44,11 +46,21 @@ def dispatch_event(event: event_builder.Event) -> None: event: Object holding information about the request to be dispatched to the Optimizely backend. """ try: + session = requests.Session() + + retries = Retry(total=EventDispatchConfig.REQUEST_TIMEOUT, + backoff_factor=0.1, + status_forcelist=[ 500, 502, 503, 504 ]) + adapter = HTTPAdapter(max_retries=retries) + + session.mount('http://', adapter) + session.mount("https://", adapter) + if event.http_verb == HTTPVerbs.GET: - requests.get(event.url, params=event.params, + session.get(event.url, params=event.params, timeout=EventDispatchConfig.REQUEST_TIMEOUT).raise_for_status() elif event.http_verb == HTTPVerbs.POST: - requests.post( + session.post( event.url, data=json.dumps(event.params), headers=event.headers, timeout=EventDispatchConfig.REQUEST_TIMEOUT, ).raise_for_status() diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 1c7a8e1c..a62d3b27 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -198,6 +198,7 @@ class VersionType: class EventDispatchConfig: """Event dispatching configs.""" REQUEST_TIMEOUT: Final = 10 + RETRIES: Final = 2 class OdpEventApiConfig: From b4b0ffe0bd555c43f69eb43368db1736827f7cb7 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 14:47:51 -0600 Subject: [PATCH 02/14] Fix linting issues --- optimizely/config_manager.py | 8 ++++---- optimizely/event_dispatcher.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index 8c3838f3..ef8b92a0 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -400,12 +400,12 @@ def fetch_datafile(self) -> None: retries = Retry(total=self.retries, backoff_factor=0.1, - status_forcelist=[ 500, 502, 503, 504 ]) + status_forcelist=[500, 502, 503, 504]) adapter = HTTPAdapter(max_retries=retries) session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return @@ -492,12 +492,12 @@ def fetch_datafile(self) -> None: retries = Retry(total=self.retries, backoff_factor=0.1, - status_forcelist=[ 500, 502, 503, 504 ]) + status_forcelist=[500, 502, 503, 504]) adapter = HTTPAdapter(max_retries=retries) session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return diff --git a/optimizely/event_dispatcher.py b/optimizely/event_dispatcher.py index 94b01072..9c8e4ce9 100644 --- a/optimizely/event_dispatcher.py +++ b/optimizely/event_dispatcher.py @@ -50,7 +50,7 @@ def dispatch_event(event: event_builder.Event) -> None: retries = Retry(total=EventDispatchConfig.REQUEST_TIMEOUT, backoff_factor=0.1, - status_forcelist=[ 500, 502, 503, 504 ]) + status_forcelist=[500, 502, 503, 504]) adapter = HTTPAdapter(max_retries=retries) session.mount('http://', adapter) @@ -58,7 +58,7 @@ def dispatch_event(event: event_builder.Event) -> None: if event.http_verb == HTTPVerbs.GET: session.get(event.url, params=event.params, - timeout=EventDispatchConfig.REQUEST_TIMEOUT).raise_for_status() + timeout=EventDispatchConfig.REQUEST_TIMEOUT).raise_for_status() elif event.http_verb == HTTPVerbs.POST: session.post( event.url, data=json.dumps(event.params), headers=event.headers, From ef9d993a2ca6df10f8f3288e46cb3aeda0f4b6d5 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 15:26:00 -0600 Subject: [PATCH 03/14] Remove print statements --- optimizely/config_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index ef8b92a0..20e3056b 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -395,7 +395,6 @@ def fetch_datafile(self) -> None: request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified try: - print(f"Getting {self.datafile_url}") session = requests.Session() retries = Retry(total=self.retries, @@ -487,7 +486,6 @@ def fetch_datafile(self) -> None: request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified try: - print(f"Getting {self.datafile_url}") session = requests.Session() retries = Retry(total=self.retries, From 80bc26cba57f290fc9056c0e7fd6074ed38667fb Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 15:33:44 -0600 Subject: [PATCH 04/14] Fix up 'retries' member --- optimizely/config_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index 20e3056b..952b3d42 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -225,6 +225,7 @@ def __init__( JSON schema validation will be performed. """ + self.retries = retries self._config_ready_event = threading.Event() super().__init__( datafile=datafile, @@ -247,7 +248,6 @@ def __init__( self.stopped = threading.Event() self._initialize_thread() self._polling_thread.start() - self.retries = retries @staticmethod def get_datafile_url(sdk_key: Optional[str], url: Optional[str], url_template: Optional[str]) -> str: From f4c8173282b5fce839f5d078e567c1085a5b2eb5 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 15:52:05 -0600 Subject: [PATCH 05/14] Stub out requests.Session.get instead of requests.get --- tests/test_config_manager.py | 24 +++++++++++----------- tests/test_event_dispatcher.py | 2 +- tests/test_notification_center_registry.py | 2 +- tests/test_optimizely.py | 6 +++--- tests/test_optimizely_factory.py | 10 ++++----- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 1c3fbe89..4877bb85 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -218,7 +218,7 @@ def test_get_config_blocks(self): self.assertEqual(1, round(end_time - start_time)) -@mock.patch('requests.get') +@mock.patch('requests.Session.get') class PollingConfigManagerTest(base.BaseTest): def test_init__no_sdk_key_no_datafile__fails(self, _): """ Test that initialization fails if there is no sdk_key or datafile provided. """ @@ -379,7 +379,7 @@ def test_fetch_datafile(self, _): test_response.status_code = 200 test_response.headers = test_headers test_response._content = test_datafile - with mock.patch('requests.get', return_value=test_response) as mock_request: + with mock.patch('requests.Session.get', return_value=test_response) as mock_request: project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key) project_config_manager.stop() @@ -392,7 +392,7 @@ def test_fetch_datafile(self, _): self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig) # Call fetch_datafile again and assert that request to URL is with If-Modified-Since header. - with mock.patch('requests.get', return_value=test_response) as mock_requests: + with mock.patch('requests.Session.get', return_value=test_response) as mock_requests: project_config_manager._initialize_thread() project_config_manager.start() project_config_manager.stop() @@ -421,7 +421,7 @@ def raise_for_status(self): test_response.headers = test_headers test_response._content = test_datafile - with mock.patch('requests.get', return_value=test_response) as mock_request: + with mock.patch('requests.Session.get', return_value=test_response) as mock_request: project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger) project_config_manager.stop() @@ -434,7 +434,7 @@ def raise_for_status(self): self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig) # Call fetch_datafile again, but raise exception this time - with mock.patch('requests.get', return_value=MockExceptionResponse()) as mock_requests: + with mock.patch('requests.Session.get', return_value=MockExceptionResponse()) as mock_requests: project_config_manager._initialize_thread() project_config_manager.start() project_config_manager.stop() @@ -462,7 +462,7 @@ def test_fetch_datafile__request_exception_raised(self, _): test_response.status_code = 200 test_response.headers = test_headers test_response._content = test_datafile - with mock.patch('requests.get', return_value=test_response) as mock_request: + with mock.patch('requests.Session.get', return_value=test_response) as mock_request: project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger) project_config_manager.stop() @@ -476,7 +476,7 @@ def test_fetch_datafile__request_exception_raised(self, _): # Call fetch_datafile again, but raise exception this time with mock.patch( - 'requests.get', + 'requests.Session.get', side_effect=requests.exceptions.RequestException('Error Error !!'), ) as mock_requests: project_config_manager._initialize_thread() @@ -506,7 +506,7 @@ def test_fetch_datafile__exception_polling_thread_failed(self, _): test_response.headers = test_headers test_response._content = test_datafile - with mock.patch('requests.get', return_value=test_response): + with mock.patch('requests.Session.get', return_value=test_response): project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key, logger=mock_logger, update_interval=12345678912345) @@ -529,7 +529,7 @@ def test_is_running(self, _): project_config_manager.stop() -@mock.patch('requests.get') +@mock.patch('requests.Session.get') class AuthDatafilePollingConfigManagerTest(base.BaseTest): def test_init__datafile_access_token_none__fails(self, _): """ Test that initialization fails if datafile_access_token is None. """ @@ -569,7 +569,7 @@ def test_fetch_datafile(self, _): test_response._content = test_datafile # Call fetch_datafile and assert that request was sent with correct authorization header - with mock.patch('requests.get', + with mock.patch('requests.Session.get', return_value=test_response) as mock_request: project_config_manager.fetch_datafile() @@ -596,7 +596,7 @@ def test_fetch_datafile__request_exception_raised(self, _): test_response._content = test_datafile # Call fetch_datafile and assert that request was sent with correct authorization header - with mock.patch('requests.get', return_value=test_response) as mock_request: + with mock.patch('requests.Session.get', return_value=test_response) as mock_request: project_config_manager = config_manager.AuthDatafilePollingConfigManager( datafile_access_token=datafile_access_token, sdk_key=sdk_key, @@ -614,7 +614,7 @@ def test_fetch_datafile__request_exception_raised(self, _): # Call fetch_datafile again, but raise exception this time with mock.patch( - 'requests.get', + 'requests.Session.get', side_effect=requests.exceptions.RequestException('Error Error !!'), ) as mock_requests: project_config_manager._initialize_thread() diff --git a/tests/test_event_dispatcher.py b/tests/test_event_dispatcher.py index 7e075f47..37be623b 100644 --- a/tests/test_event_dispatcher.py +++ b/tests/test_event_dispatcher.py @@ -29,7 +29,7 @@ def test_dispatch_event__get_request(self): params = {'a': '111001', 'n': 'test_event', 'g': '111028', 'u': 'oeutest_user'} event = event_builder.Event(url, params) - with mock.patch('requests.get') as mock_request_get: + with mock.patch('requests.Session.get') as mock_request_get: event_dispatcher.EventDispatcher.dispatch_event(event) mock_request_get.assert_called_once_with(url, params=params, timeout=EventDispatchConfig.REQUEST_TIMEOUT) diff --git a/tests/test_notification_center_registry.py b/tests/test_notification_center_registry.py index 0f800cfd..81984059 100644 --- a/tests/test_notification_center_registry.py +++ b/tests/test_notification_center_registry.py @@ -60,7 +60,7 @@ def test_remove_notification_center(self): test_response = self.fake_server_response(status_code=200, content=test_datafile) notification_center = _NotificationCenterRegistry.get_notification_center(sdk_key, logger) - with mock.patch('requests.get', return_value=test_response), \ + with mock.patch('requests.Session.get', return_value=test_response), \ mock.patch.object(notification_center, 'send_notifications') as mock_send: client = Optimizely(sdk_key=sdk_key, logger=logger) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 8d36b830..1f4293cd 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -4696,7 +4696,7 @@ def delay(*args, **kwargs): time.sleep(.5) return mock.DEFAULT - with mock.patch('requests.get', return_value=test_response, side_effect=delay): + with mock.patch('requests.Session.get', return_value=test_response, side_effect=delay): # initialize config_manager with delay, so it will receive the datafile after client initialization custom_config_manager = config_manager.PollingConfigManager(sdk_key='segments-test', logger=logger) client = optimizely.Optimizely(config_manager=custom_config_manager) @@ -5428,7 +5428,7 @@ def test_send_odp_event__send_event_with_static_config_manager(self): def test_send_odp_event__send_event_with_polling_config_manager(self): mock_logger = mock.Mock() with mock.patch( - 'requests.get', + 'requests.Session.get', return_value=self.fake_server_response( status_code=200, content=json.dumps(self.config_dict_with_audience_segments) @@ -5467,7 +5467,7 @@ def test_send_odp_event__log_debug_if_datafile_not_ready(self): def test_send_odp_event__log_error_if_odp_not_enabled_with_polling_config_manager(self): mock_logger = mock.Mock() with mock.patch( - 'requests.get', + 'requests.Session.get', return_value=self.fake_server_response( status_code=200, content=json.dumps(self.config_dict_with_audience_segments) diff --git a/tests/test_optimizely_factory.py b/tests/test_optimizely_factory.py index be41755a..989d960c 100644 --- a/tests/test_optimizely_factory.py +++ b/tests/test_optimizely_factory.py @@ -26,7 +26,7 @@ from . import base -@mock.patch('requests.get') +@mock.patch('requests.Session.get') class OptimizelyFactoryTest(base.BaseTest): def delay(*args, **kwargs): time.sleep(.5) @@ -171,7 +171,7 @@ def test_set_batch_size_and_set_flush_interval___should_set_values_valid_or_inva self.assertEqual(optimizely_instance.event_processor.batch_size, 10) def test_update_odp_config_correctly(self, _): - with mock.patch('requests.get') as mock_request_post: + with mock.patch('requests.Session.get') as mock_request_post: mock_request_post.return_value = self.fake_server_response( status_code=200, content=json.dumps(self.config_dict_with_audience_segments) @@ -194,7 +194,7 @@ def test_update_odp_config_correctly_with_custom_config_manager_and_delay(self, test_datafile = json.dumps(self.config_dict_with_audience_segments) test_response = self.fake_server_response(status_code=200, content=test_datafile) - with mock.patch('requests.get', return_value=test_response, side_effect=self.delay): + with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay): # initialize config_manager with delay, so it will receive the datafile after client initialization config_manager = PollingConfigManager(sdk_key='test', logger=logger) client = OptimizelyFactory.default_instance_with_config_manager(config_manager=config_manager) @@ -221,7 +221,7 @@ def test_update_odp_config_correctly_with_delay(self, _): test_datafile = json.dumps(self.config_dict_with_audience_segments) test_response = self.fake_server_response(status_code=200, content=test_datafile) - with mock.patch('requests.get', return_value=test_response, side_effect=self.delay): + with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay): # initialize config_manager with delay, so it will receive the datafile after client initialization client = OptimizelyFactory.default_instance(sdk_key='test') odp_manager = client.odp_manager @@ -247,7 +247,7 @@ def test_odp_updated_with_custom_instance(self, _): test_datafile = json.dumps(self.config_dict_with_audience_segments) test_response = self.fake_server_response(status_code=200, content=test_datafile) - with mock.patch('requests.get', return_value=test_response, side_effect=self.delay): + with mock.patch('requests.Session.get', return_value=test_response, side_effect=self.delay): # initialize config_manager with delay, so it will receive the datafile after client initialization client = OptimizelyFactory.custom_instance(sdk_key='test') odp_manager = client.odp_manager From 8624e1e1868598cf525a5ed23895a60c08fb5861 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 16:19:14 -0600 Subject: [PATCH 06/14] Update tests --- optimizely/config_manager.py | 4 ++-- tests/test_config_manager.py | 3 ++- tests/test_event_dispatcher.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index 952b3d42..fdfc6459 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -404,7 +404,7 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return @@ -495,7 +495,7 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 4877bb85..56674381 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -516,8 +516,9 @@ def test_fetch_datafile__exception_polling_thread_failed(self, _): # verify the error log message log_messages = [args[0] for args, _ in mock_logger.error.call_args_list] for message in log_messages: + print(message) if "Thread for background datafile polling failed. " \ - "Error: timestamp too large to convert to C _PyTime_t" not in message: + "Error: timestamp too large to convert to C PyTime_t" not in message: assert False def test_is_running(self, _): diff --git a/tests/test_event_dispatcher.py b/tests/test_event_dispatcher.py index 37be623b..30311e35 100644 --- a/tests/test_event_dispatcher.py +++ b/tests/test_event_dispatcher.py @@ -46,7 +46,7 @@ def test_dispatch_event__post_request(self): } event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) - with mock.patch('requests.post') as mock_request_post: + with mock.patch('requests.Session.post') as mock_request_post: event_dispatcher.EventDispatcher.dispatch_event(event) mock_request_post.assert_called_once_with( @@ -69,7 +69,7 @@ def test_dispatch_event__handle_request_exception(self): event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) with mock.patch( - 'requests.post', side_effect=request_exception.RequestException('Failed Request'), + 'requests.Session.post', side_effect=request_exception.RequestException('Failed Request'), ) as mock_request_post, mock.patch('logging.error') as mock_log_error: event_dispatcher.EventDispatcher.dispatch_event(event) From d5ed5695e42578c1a55ae600c39300ed64f7668e Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 16:33:31 -0600 Subject: [PATCH 07/14] Fix mypy error and linting error --- optimizely/config_manager.py | 8 ++++++-- optimizely/helpers/validator.py | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index fdfc6459..916fc398 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -404,7 +404,9 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, + headers=request_headers, + timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return @@ -495,7 +497,9 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = session.get(self.datafile_url, + headers=request_headers, + timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') return diff --git a/optimizely/helpers/validator.py b/optimizely/helpers/validator.py index 17cff87c..f72e8334 100644 --- a/optimizely/helpers/validator.py +++ b/optimizely/helpers/validator.py @@ -276,8 +276,9 @@ def is_finite_number(value: Any) -> bool: if math.isnan(value) or math.isinf(value): return False - if abs(value) > (2 ** 53): - return False + if isinstance(value, int): + if abs(value) > (2 ** 53): + return False return True From 44f51318b84725cfc7e73983fe7a0f2ea32dbf64 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 16:38:48 -0600 Subject: [PATCH 08/14] Update for tests --- optimizely/helpers/validator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optimizely/helpers/validator.py b/optimizely/helpers/validator.py index f72e8334..b9e4fcc5 100644 --- a/optimizely/helpers/validator.py +++ b/optimizely/helpers/validator.py @@ -276,9 +276,9 @@ def is_finite_number(value: Any) -> bool: if math.isnan(value) or math.isinf(value): return False - if isinstance(value, int): - if abs(value) > (2 ** 53): - return False + if isinstance(value, (int, float)): + if abs(value) > (2 ** 53): + return False return True From b7a585fdff59b0b7beb565f352086470ae87cc54 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Mon, 24 Feb 2025 16:41:37 -0600 Subject: [PATCH 09/14] Update --- optimizely/config_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index 916fc398..3dce2741 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -404,8 +404,8 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, - headers=request_headers, + response = session.get(self.datafile_url, + headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') @@ -497,8 +497,8 @@ def fetch_datafile(self) -> None: session.mount('http://', adapter) session.mount("https://", adapter) - response = session.get(self.datafile_url, - headers=request_headers, + response = session.get(self.datafile_url, + headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT) except requests_exceptions.RequestException as err: self.logger.error(f'Fetching datafile from {self.datafile_url} failed. Error: {err}') From 8d7d22f570228fcd36fedd1b9f3cc8ea4b09aaa0 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Tue, 25 Feb 2025 14:50:36 -0600 Subject: [PATCH 10/14] Update optimizely/event_dispatcher.py Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --- optimizely/event_dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizely/event_dispatcher.py b/optimizely/event_dispatcher.py index 9c8e4ce9..767fbb7d 100644 --- a/optimizely/event_dispatcher.py +++ b/optimizely/event_dispatcher.py @@ -48,7 +48,7 @@ def dispatch_event(event: event_builder.Event) -> None: try: session = requests.Session() - retries = Retry(total=EventDispatchConfig.REQUEST_TIMEOUT, + retries = Retry(total=EventDispatchConfig.RETRIES, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504]) adapter = HTTPAdapter(max_retries=retries) From 20a1f20f0cc0b5fdd9516649b34834fadbd87205 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Tue, 25 Feb 2025 14:52:07 -0600 Subject: [PATCH 11/14] Update event dispatch to try three times to send events --- optimizely/helpers/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index a62d3b27..fe90946e 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -198,7 +198,7 @@ class VersionType: class EventDispatchConfig: """Event dispatching configs.""" REQUEST_TIMEOUT: Final = 10 - RETRIES: Final = 2 + RETRIES: Final = 3 class OdpEventApiConfig: From 551cb5e0573fcdfc04bbd8475960fe7c3b8ffa6d Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Wed, 26 Feb 2025 08:21:12 -0600 Subject: [PATCH 12/14] Update changelog and version number --- CHANGELOG.md | 5 +++++ optimizely/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f3bc3cb..3cc590b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Optimizely Python SDK Changelog +## 5.1.1 +February 26, 2025 + +`PollingConfigManager` now has another optional parameter `retries` that will control how many times the SDK will attempt to get the datafile if the connection fails. Previously, the SDK would only try once. Now it defaults to maximum of three attempts. When sending event data, the SDK will attempt to send event data up to three times, where as before it would only attempt once. + ## 5.1.0 November 27th, 2024 diff --git a/optimizely/version.py b/optimizely/version.py index 941e5e68..5fe9b160 100644 --- a/optimizely/version.py +++ b/optimizely/version.py @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version_info = (5, 1, 0) +version_info = (5, 1, 1) __version__ = '.'.join(str(v) for v in version_info) From 375ec2dd674ca04c42803fe5c42734d70d5170cf Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Wed, 26 Feb 2025 09:39:36 -0600 Subject: [PATCH 13/14] Update version number --- CHANGELOG.md | 2 +- optimizely/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc590b9..e6444bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Optimizely Python SDK Changelog -## 5.1.1 +## 5.2.0 February 26, 2025 `PollingConfigManager` now has another optional parameter `retries` that will control how many times the SDK will attempt to get the datafile if the connection fails. Previously, the SDK would only try once. Now it defaults to maximum of three attempts. When sending event data, the SDK will attempt to send event data up to three times, where as before it would only attempt once. diff --git a/optimizely/version.py b/optimizely/version.py index 5fe9b160..4f0f20c6 100644 --- a/optimizely/version.py +++ b/optimizely/version.py @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version_info = (5, 1, 1) +version_info = (5, 2, 0) __version__ = '.'.join(str(v) for v in version_info) From 4d74b59c71de34d9457322a59335d7b5a5b8d405 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Wed, 26 Feb 2025 12:12:47 -0600 Subject: [PATCH 14/14] Remove changelog and version update --- CHANGELOG.md | 5 ----- optimizely/version.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6444bd0..7f3bc3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,5 @@ # Optimizely Python SDK Changelog -## 5.2.0 -February 26, 2025 - -`PollingConfigManager` now has another optional parameter `retries` that will control how many times the SDK will attempt to get the datafile if the connection fails. Previously, the SDK would only try once. Now it defaults to maximum of three attempts. When sending event data, the SDK will attempt to send event data up to three times, where as before it would only attempt once. - ## 5.1.0 November 27th, 2024 diff --git a/optimizely/version.py b/optimizely/version.py index 4f0f20c6..941e5e68 100644 --- a/optimizely/version.py +++ b/optimizely/version.py @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version_info = (5, 2, 0) +version_info = (5, 1, 0) __version__ = '.'.join(str(v) for v in version_info)