Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "^.secrets.baseline$",
"lines": null
},
"generated_at": "2025-09-24T13:09:33Z",
"generated_at": "2025-10-08T10:30:25Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -61,7 +61,7 @@
}
],
"results": {},
"version": "0.13.1+ibm.62.dss",
"version": "0.13.1+ibm.64.dss",
"word_list": {
"file": null,
"hash": null
Expand Down
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,6 @@ def configuration_update(self):
appconfig_client.register_configuration_update_listener(configuration_update)
```

## Fetch latest data

Fetch the latest configuration data.

```py
appconfig_client.fetch_configurations()
```

## Enable debugger (Optional)

Use this method to enable/disable the logging in SDK.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ibm\_appconfiguration.configurations.internal.utils.compute\_percentage module
==============================================================================

.. automodule:: ibm_appconfiguration.configurations.internal.utils.compute_percentage
:members:
:undoc-members:
:show-inheritance:
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ibm\_appconfiguration.configurations.internal.utils.parser module
=================================================================

.. automodule:: ibm_appconfiguration.configurations.internal.utils.parser
:members:
:undoc-members:
:show-inheritance:
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ Submodules
:maxdepth: 4

ibm_appconfiguration.configurations.internal.utils.api_manager
ibm_appconfiguration.configurations.internal.utils.compute_percentage
ibm_appconfiguration.configurations.internal.utils.connectivity
ibm_appconfiguration.configurations.internal.utils.file_manager
ibm_appconfiguration.configurations.internal.utils.logger
ibm_appconfiguration.configurations.internal.utils.metering
ibm_appconfiguration.configurations.internal.utils.parser
ibm_appconfiguration.configurations.internal.utils.socket
ibm_appconfiguration.configurations.internal.utils.url_builder
ibm_appconfiguration.configurations.internal.utils.validators
Expand Down
18 changes: 1 addition & 17 deletions ibm_appconfiguration/appconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def __init__(self):
self.__guid = ''
self.__is_initialized = False
self.__is_initialized_configuration = False
self.__is_loading = False
AppConfiguration.__instance = self

def use_private_endpoint(self, use_private_endpoint_param: bool):
Expand Down Expand Up @@ -131,7 +130,6 @@ def init(self, region: str, guid: str, apikey: str):
self.__region = region
self.__guid = guid
self.__is_initialized = True
self.__is_loading = False
self.__setup_configuration_handler()

def get_region(self) -> str:
Expand Down Expand Up @@ -216,28 +214,14 @@ def set_context(self, collection_id: str, environment_id: str, configuration_fil
self.__is_initialized_configuration = True

self.__configuration_handler_instance.set_context(collection_id, environment_id, default_options)
self.__load_data_now()

def fetch_configurations(self):
"""Fetch the latest configurations"""
if self.__is_initialized and self.__is_initialized_configuration:
self.__load_data_now()
else:
Logger.error(config_messages.COLLECTION_INIT_ERROR)
self.__configuration_handler_instance.load_data()

def __setup_configuration_handler(self):
self.__configuration_handler_instance = ConfigurationHandler.get_instance()
self.__configuration_handler_instance.init(region=self.__region, guid=self.__guid, apikey=self.__apikey,
override_service_url=self.__override_service_url,
use_private_endpoint=self.__use_private_endpoint)

def __load_data_now(self):
if self.__is_loading:
return
self.__is_loading = True
self.__configuration_handler_instance.load_data()
self.__is_loading = False

def register_configuration_update_listener(self, listener):
"""Register a listener for the Configuration changes.

Expand Down
78 changes: 9 additions & 69 deletions ibm_appconfiguration/configurations/configuration_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from typing import Dict, List, Any
from threading import Timer, Thread
from ibm_appconfiguration.configurations.internal.common import config_messages, config_constants
from ibm_appconfiguration.version import __version__
from .internal.utils.logger import Logger
from .internal.utils.parser import extract_configurations, format_config
from .internal.utils.validators import Validators
Expand All @@ -33,25 +32,13 @@
from .internal.utils.metering import Metering
from .internal.utils.socket import Socket
from .internal.utils.url_builder import URLBuilder
from .internal.utils.connectivity import Connectivity
from .internal.utils.api_manager import APIManager
import sys
from time import sleep

# Server max time out is assumed to be 1 week = 604800 seconds = 40320*15
sys.setrecursionlimit(40320)

# delay between each web socket connection retry
delay = 15


class ConfigurationHandler:
"""Internal class to handle the configuration"""
__instance = None

# variable to keep track of server-client connection status
__is_alive = False

@staticmethod
def get_instance():
""" Static access method. """
Expand Down Expand Up @@ -83,8 +70,6 @@ def __init__(self):
self.__on_socket_retry = False
self.__override_service_url = None
self.__socket = None
self.__connectivity = None
self.__is_network_connected = True
self.__api_manager = None
self.__use_private_endpoint = False

Expand Down Expand Up @@ -139,7 +124,6 @@ def set_context(self, collection_id: str, environment_id: str, options: dict):
self.__bootstrap_file = options['bootstrap_file']
self.__persistent_cache_dir = options['persistent_cache_dir']
self.__is_initialized = True
self.__check_network()

def load_data(self):
"""Load the configuration data"""
Expand Down Expand Up @@ -202,28 +186,6 @@ def register_configuration_update_listener(self, listener):
else:
Logger.error(config_messages.CONFIGURATION_HANDLER_METHOD_ERROR)

def __check_network(self):
if self.__live_config_update_enabled:
if self.__connectivity is None:
self.__connectivity = Connectivity.get_instance()
self.__connectivity.add_connectivity_listener(self.__network_listener)
self.__connectivity.check_connection()
else:
self.__connectivity = None

def __network_listener(self, is_connected: bool):
if not self.__live_config_update_enabled:
self.__connectivity = None
return

if is_connected:
if not self.__is_network_connected:
self.__is_network_connected = True
self.__fetch_config_data()
else:
Logger.debug(config_messages.NO_INTERNET_CONNECTION_ERROR)
self.__is_network_connected = False

def get_properties(self) -> Dict[str, Property]:
"""Get the list of Property objects

Expand Down Expand Up @@ -272,25 +234,15 @@ def __fetch_config_data(self):
if self.__is_initialized:
self.__fetch_from_api()
self.__on_socket_retry = False
# Socket connection is a long-running background task, and is safe to run as daemon threads
config_thread = Thread(target=self.__start_web_socket, args=())
config_thread.daemon = True
config_thread.start()
self.__start_web_socket()

def __start_web_socket(self):
bearer_token = URLBuilder.get_iam_authenticator().token_manager.get_token()
headers = {
'Authorization': 'Bearer ' + bearer_token,
'User-Agent': '{0}/{1}'.format(config_constants.SDK_NAME, __version__)
}
if self.__socket:
self.__socket.cancel()
self.__socket = None

self.__socket = Socket()
self.__socket.setup(
url=URLBuilder.get_web_socket_url(),
headers=headers,
callback=self.__on_web_socket_callback
headers_provider=self.__api_manager.get_websocket_headers,
callback=self.__handle_socket_events
)

def __load_configurations(self, data: dict):
Expand Down Expand Up @@ -548,32 +500,20 @@ def __fetch_from_api(self):
else:
Logger.debug(config_messages.CONFIGURATION_HANDLER_INIT_ERROR)

def __on_web_socket_callback(self, message=None, error_state=None,
closed_state=None, open_state=None):
def __handle_socket_events(self, message=None, error_state=None,
closed_state=None, open_state=None):
if message:
self.__is_alive = True
Logger.debug(f'Received message from websocket. {message}')
self.__fetch_from_api()
Logger.debug(f'Received message from socket. {message}')
elif error_state:
self.__is_alive = False
Logger.error(f'Received error from socket. {error_state}')
Logger.info('Reconnecting to server....')
self.__on_socket_retry = True
sleep(delay)
self.__start_web_socket()
elif closed_state:
self.__is_alive = False
Logger.error('Received close connection from socket.')
Logger.info('Reconnecting to server....')
self.__on_socket_retry = True
sleep(delay)
self.__start_web_socket()
elif open_state:
self.__is_alive = True
Logger.debug('Received opened connection from websocket.')
if self.__on_socket_retry:
self.__on_socket_retry = False
self.__fetch_from_api()
Logger.debug('Received opened connection from socket.')
else:
Logger.error('Unknown Error inside the socket connection.')

Expand All @@ -582,4 +522,4 @@ def is_connected(self) -> bool:

Returns: boolean indicating connection status
"""
return self.__is_alive
return self.__socket.is_connected()
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@
DEFAULT_ROLLOUT_PERCENTAGE = '$default'
DEFAULT_FEATURE_VALUE = '$default'
DEFAULT_PROPERTY_VALUE = '$default'
WEBSOCKET_RECONNECT_DELAY = 15 # Constant delay between reconnection attempts for server errors
CUSTOM_SOCKET_CLOSE_REASON_CODE = 4001
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
CONFIGURATION_HANDLER_METHOD_ERROR = "Invalid action in ConfigurationHandler. Should be a method/function"
SINGLETON_EXCEPTION = "class must be initialized using the get_instance() method."
FEATURE_INVALID = "Invalid feature_id - "
NO_INTERNET_CONNECTION_ERROR = 'No connection to internet. Please re-connect.'
PROPERTY_INVALID = "Invalid property_id - "
CONFIGURATIONS_FETCH_SUCCESS = "Successfully fetched the configurations."
RETRY_AFTER_TWO_MINUTES = "Failed to fetch the configurations. Retrying after 2 minutes."
Expand Down
25 changes: 25 additions & 0 deletions ibm_appconfiguration/configurations/internal/utils/api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from typing import Optional, Union
from ibm_cloud_sdk_core import BaseService, DetailedResponse, ApiException
from requests.exceptions import RetryError

from .logger import Logger
from .url_builder import URLBuilder
from ibm_appconfiguration.version import __version__
from ..common import config_constants
Expand Down Expand Up @@ -99,3 +101,26 @@ def __remove_null_values(self, dictionary: dict) -> dict:
if isinstance(dictionary, dict):
return {k: v for (k, v) in dictionary.items() if v is not None}
return dictionary

def get_websocket_headers(self) -> dict:
"""Get fresh headers for WebSocket connection with current authentication token.
This method retrieves a fresh authentication token and returns headers
suitable for WebSocket connections. It should be called each time a
WebSocket connection is established to ensure the token is valid.

Returns:
dict: Headers dictionary containing Authorization and User-Agent

Raises:
Exception: If token retrieval fails, the exception is propagated
to allow the caller to determine if reconnection should be attempted
"""
try:
bearer_token = URLBuilder.get_iam_authenticator().token_manager.get_token()
return {
'Authorization': 'Bearer ' + bearer_token,
'User-Agent': '{0}/{1}'.format(config_constants.SDK_NAME, __version__)
}
except Exception as e:
Logger.error(f"Failed to retrieve IAM token for WebSocket: {str(e)}")
raise
Loading