ibm-appconfiguration-python-sdk 0.3.9__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ibm_appconfiguration_python_sdk-0.3.9/ibm_appconfiguration_python_sdk.egg-info → ibm_appconfiguration_python_sdk-0.4.1}/PKG-INFO +1 -9
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/README.md +0 -8
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/appconfiguration.py +1 -17
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/configuration_handler.py +9 -69
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/config_constants.py +2 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/config_messages.py +0 -1
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/api_manager.py +25 -0
- ibm_appconfiguration_python_sdk-0.4.1/ibm_appconfiguration/configurations/internal/utils/socket.py +261 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/configuration_type.py +1 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/version.py +1 -1
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1/ibm_appconfiguration_python_sdk.egg-info}/PKG-INFO +1 -9
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/setup.py +1 -1
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_socket.py +4 -1
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/test_appconfiguration.py +0 -4
- ibm_appconfiguration_python_sdk-0.3.9/ibm_appconfiguration/configurations/internal/utils/socket.py +0 -78
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/LICENSE +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/sample_app.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/server_sample.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/compute_percentage.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/connectivity.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/file_manager.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/logger.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/metering.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/parser.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/url_builder.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/validators.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/feature.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/property.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/rule.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/segment.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/segment_rules.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/SOURCES.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/dependency_links.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/requires.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/top_level.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/integration_tests/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/integration_tests/test_integration.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/setup.cfg +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_feature.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_property.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_rule.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_segment.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_segment_rules.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/test_configuration_handler.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_api_manager.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_file_manager.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_metering.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_url_builder.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ibm-appconfiguration-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: IBM Cloud App Configuration Python SDK
|
|
5
5
|
Home-page: https://github.com/IBM/appconfiguration-python-sdk
|
|
6
6
|
Author: IBM
|
|
@@ -357,14 +357,6 @@ def configuration_update(self):
|
|
|
357
357
|
appconfig_client.register_configuration_update_listener(configuration_update)
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
## Fetch latest data
|
|
361
|
-
|
|
362
|
-
Fetch the latest configuration data.
|
|
363
|
-
|
|
364
|
-
```py
|
|
365
|
-
appconfig_client.fetch_configurations()
|
|
366
|
-
```
|
|
367
|
-
|
|
368
360
|
## Enable debugger (Optional)
|
|
369
361
|
|
|
370
362
|
Use this method to enable/disable the logging in SDK.
|
|
@@ -321,14 +321,6 @@ def configuration_update(self):
|
|
|
321
321
|
appconfig_client.register_configuration_update_listener(configuration_update)
|
|
322
322
|
```
|
|
323
323
|
|
|
324
|
-
## Fetch latest data
|
|
325
|
-
|
|
326
|
-
Fetch the latest configuration data.
|
|
327
|
-
|
|
328
|
-
```py
|
|
329
|
-
appconfig_client.fetch_configurations()
|
|
330
|
-
```
|
|
331
|
-
|
|
332
324
|
## Enable debugger (Optional)
|
|
333
325
|
|
|
334
326
|
Use this method to enable/disable the logging in SDK.
|
|
@@ -89,7 +89,6 @@ class AppConfiguration:
|
|
|
89
89
|
self.__guid = ''
|
|
90
90
|
self.__is_initialized = False
|
|
91
91
|
self.__is_initialized_configuration = False
|
|
92
|
-
self.__is_loading = False
|
|
93
92
|
AppConfiguration.__instance = self
|
|
94
93
|
|
|
95
94
|
def use_private_endpoint(self, use_private_endpoint_param: bool):
|
|
@@ -131,7 +130,6 @@ class AppConfiguration:
|
|
|
131
130
|
self.__region = region
|
|
132
131
|
self.__guid = guid
|
|
133
132
|
self.__is_initialized = True
|
|
134
|
-
self.__is_loading = False
|
|
135
133
|
self.__setup_configuration_handler()
|
|
136
134
|
|
|
137
135
|
def get_region(self) -> str:
|
|
@@ -216,14 +214,7 @@ class AppConfiguration:
|
|
|
216
214
|
self.__is_initialized_configuration = True
|
|
217
215
|
|
|
218
216
|
self.__configuration_handler_instance.set_context(collection_id, environment_id, default_options)
|
|
219
|
-
self.
|
|
220
|
-
|
|
221
|
-
def fetch_configurations(self):
|
|
222
|
-
"""Fetch the latest configurations"""
|
|
223
|
-
if self.__is_initialized and self.__is_initialized_configuration:
|
|
224
|
-
self.__load_data_now()
|
|
225
|
-
else:
|
|
226
|
-
Logger.error(config_messages.COLLECTION_INIT_ERROR)
|
|
217
|
+
self.__configuration_handler_instance.load_data()
|
|
227
218
|
|
|
228
219
|
def __setup_configuration_handler(self):
|
|
229
220
|
self.__configuration_handler_instance = ConfigurationHandler.get_instance()
|
|
@@ -231,13 +222,6 @@ class AppConfiguration:
|
|
|
231
222
|
override_service_url=self.__override_service_url,
|
|
232
223
|
use_private_endpoint=self.__use_private_endpoint)
|
|
233
224
|
|
|
234
|
-
def __load_data_now(self):
|
|
235
|
-
if self.__is_loading:
|
|
236
|
-
return
|
|
237
|
-
self.__is_loading = True
|
|
238
|
-
self.__configuration_handler_instance.load_data()
|
|
239
|
-
self.__is_loading = False
|
|
240
|
-
|
|
241
225
|
def register_configuration_update_listener(self, listener):
|
|
242
226
|
"""Register a listener for the Configuration changes.
|
|
243
227
|
|
|
@@ -20,7 +20,6 @@ import os
|
|
|
20
20
|
from typing import Dict, List, Any
|
|
21
21
|
from threading import Timer, Thread
|
|
22
22
|
from ibm_appconfiguration.configurations.internal.common import config_messages, config_constants
|
|
23
|
-
from ibm_appconfiguration.version import __version__
|
|
24
23
|
from .internal.utils.logger import Logger
|
|
25
24
|
from .internal.utils.parser import extract_configurations, format_config
|
|
26
25
|
from .internal.utils.validators import Validators
|
|
@@ -33,25 +32,13 @@ from .internal.utils.compute_percentage import get_normalized_value
|
|
|
33
32
|
from .internal.utils.metering import Metering
|
|
34
33
|
from .internal.utils.socket import Socket
|
|
35
34
|
from .internal.utils.url_builder import URLBuilder
|
|
36
|
-
from .internal.utils.connectivity import Connectivity
|
|
37
35
|
from .internal.utils.api_manager import APIManager
|
|
38
|
-
import sys
|
|
39
|
-
from time import sleep
|
|
40
|
-
|
|
41
|
-
# Server max time out is assumed to be 1 week = 604800 seconds = 40320*15
|
|
42
|
-
sys.setrecursionlimit(40320)
|
|
43
|
-
|
|
44
|
-
# delay between each web socket connection retry
|
|
45
|
-
delay = 15
|
|
46
36
|
|
|
47
37
|
|
|
48
38
|
class ConfigurationHandler:
|
|
49
39
|
"""Internal class to handle the configuration"""
|
|
50
40
|
__instance = None
|
|
51
41
|
|
|
52
|
-
# variable to keep track of server-client connection status
|
|
53
|
-
__is_alive = False
|
|
54
|
-
|
|
55
42
|
@staticmethod
|
|
56
43
|
def get_instance():
|
|
57
44
|
""" Static access method. """
|
|
@@ -83,8 +70,6 @@ class ConfigurationHandler:
|
|
|
83
70
|
self.__on_socket_retry = False
|
|
84
71
|
self.__override_service_url = None
|
|
85
72
|
self.__socket = None
|
|
86
|
-
self.__connectivity = None
|
|
87
|
-
self.__is_network_connected = True
|
|
88
73
|
self.__api_manager = None
|
|
89
74
|
self.__use_private_endpoint = False
|
|
90
75
|
|
|
@@ -139,7 +124,6 @@ class ConfigurationHandler:
|
|
|
139
124
|
self.__bootstrap_file = options['bootstrap_file']
|
|
140
125
|
self.__persistent_cache_dir = options['persistent_cache_dir']
|
|
141
126
|
self.__is_initialized = True
|
|
142
|
-
self.__check_network()
|
|
143
127
|
|
|
144
128
|
def load_data(self):
|
|
145
129
|
"""Load the configuration data"""
|
|
@@ -202,28 +186,6 @@ class ConfigurationHandler:
|
|
|
202
186
|
else:
|
|
203
187
|
Logger.error(config_messages.CONFIGURATION_HANDLER_METHOD_ERROR)
|
|
204
188
|
|
|
205
|
-
def __check_network(self):
|
|
206
|
-
if self.__live_config_update_enabled:
|
|
207
|
-
if self.__connectivity is None:
|
|
208
|
-
self.__connectivity = Connectivity.get_instance()
|
|
209
|
-
self.__connectivity.add_connectivity_listener(self.__network_listener)
|
|
210
|
-
self.__connectivity.check_connection()
|
|
211
|
-
else:
|
|
212
|
-
self.__connectivity = None
|
|
213
|
-
|
|
214
|
-
def __network_listener(self, is_connected: bool):
|
|
215
|
-
if not self.__live_config_update_enabled:
|
|
216
|
-
self.__connectivity = None
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
if is_connected:
|
|
220
|
-
if not self.__is_network_connected:
|
|
221
|
-
self.__is_network_connected = True
|
|
222
|
-
self.__fetch_config_data()
|
|
223
|
-
else:
|
|
224
|
-
Logger.debug(config_messages.NO_INTERNET_CONNECTION_ERROR)
|
|
225
|
-
self.__is_network_connected = False
|
|
226
|
-
|
|
227
189
|
def get_properties(self) -> Dict[str, Property]:
|
|
228
190
|
"""Get the list of Property objects
|
|
229
191
|
|
|
@@ -272,25 +234,15 @@ class ConfigurationHandler:
|
|
|
272
234
|
if self.__is_initialized:
|
|
273
235
|
self.__fetch_from_api()
|
|
274
236
|
self.__on_socket_retry = False
|
|
275
|
-
|
|
276
|
-
config_thread = Thread(target=self.__start_web_socket, args=())
|
|
277
|
-
config_thread.daemon = True
|
|
278
|
-
config_thread.start()
|
|
237
|
+
self.__start_web_socket()
|
|
279
238
|
|
|
280
239
|
def __start_web_socket(self):
|
|
281
|
-
|
|
282
|
-
headers = {
|
|
283
|
-
'Authorization': 'Bearer ' + bearer_token,
|
|
284
|
-
'User-Agent': '{0}/{1}'.format(config_constants.SDK_NAME, __version__)
|
|
285
|
-
}
|
|
286
|
-
if self.__socket:
|
|
287
|
-
self.__socket.cancel()
|
|
288
|
-
self.__socket = None
|
|
240
|
+
|
|
289
241
|
self.__socket = Socket()
|
|
290
242
|
self.__socket.setup(
|
|
291
243
|
url=URLBuilder.get_web_socket_url(),
|
|
292
|
-
|
|
293
|
-
callback=self.
|
|
244
|
+
headers_provider=self.__api_manager.get_websocket_headers,
|
|
245
|
+
callback=self.__handle_socket_events
|
|
294
246
|
)
|
|
295
247
|
|
|
296
248
|
def __load_configurations(self, data: dict):
|
|
@@ -548,32 +500,20 @@ class ConfigurationHandler:
|
|
|
548
500
|
else:
|
|
549
501
|
Logger.debug(config_messages.CONFIGURATION_HANDLER_INIT_ERROR)
|
|
550
502
|
|
|
551
|
-
def
|
|
552
|
-
|
|
503
|
+
def __handle_socket_events(self, message=None, error_state=None,
|
|
504
|
+
closed_state=None, open_state=None):
|
|
553
505
|
if message:
|
|
554
|
-
|
|
506
|
+
Logger.debug(f'Received message from websocket. {message}')
|
|
555
507
|
self.__fetch_from_api()
|
|
556
|
-
Logger.debug(f'Received message from socket. {message}')
|
|
557
508
|
elif error_state:
|
|
558
|
-
self.__is_alive = False
|
|
559
|
-
Logger.error(f'Received error from socket. {error_state}')
|
|
560
|
-
Logger.info('Reconnecting to server....')
|
|
561
509
|
self.__on_socket_retry = True
|
|
562
|
-
sleep(delay)
|
|
563
|
-
self.__start_web_socket()
|
|
564
510
|
elif closed_state:
|
|
565
|
-
self.__is_alive = False
|
|
566
|
-
Logger.error('Received close connection from socket.')
|
|
567
|
-
Logger.info('Reconnecting to server....')
|
|
568
511
|
self.__on_socket_retry = True
|
|
569
|
-
sleep(delay)
|
|
570
|
-
self.__start_web_socket()
|
|
571
512
|
elif open_state:
|
|
572
|
-
|
|
513
|
+
Logger.debug('Received opened connection from websocket.')
|
|
573
514
|
if self.__on_socket_retry:
|
|
574
515
|
self.__on_socket_retry = False
|
|
575
516
|
self.__fetch_from_api()
|
|
576
|
-
Logger.debug('Received opened connection from socket.')
|
|
577
517
|
else:
|
|
578
518
|
Logger.error('Unknown Error inside the socket connection.')
|
|
579
519
|
|
|
@@ -582,4 +522,4 @@ class ConfigurationHandler:
|
|
|
582
522
|
|
|
583
523
|
Returns: boolean indicating connection status
|
|
584
524
|
"""
|
|
585
|
-
return self.
|
|
525
|
+
return self.__socket.is_connected()
|
|
@@ -24,3 +24,5 @@ MAX_NUMBER_OF_RETRIES = 3
|
|
|
24
24
|
DEFAULT_ROLLOUT_PERCENTAGE = '$default'
|
|
25
25
|
DEFAULT_FEATURE_VALUE = '$default'
|
|
26
26
|
DEFAULT_PROPERTY_VALUE = '$default'
|
|
27
|
+
WEBSOCKET_RECONNECT_DELAY = 15 # Constant delay between reconnection attempts for server errors
|
|
28
|
+
CUSTOM_SOCKET_CLOSE_REASON_CODE = 4001
|
|
@@ -35,7 +35,6 @@ CONFIGURATION_HANDLER_INIT_ERROR = 'Invalid action in ConfigurationHandler. This
|
|
|
35
35
|
CONFIGURATION_HANDLER_METHOD_ERROR = "Invalid action in ConfigurationHandler. Should be a method/function"
|
|
36
36
|
SINGLETON_EXCEPTION = "class must be initialized using the get_instance() method."
|
|
37
37
|
FEATURE_INVALID = "Invalid feature_id - "
|
|
38
|
-
NO_INTERNET_CONNECTION_ERROR = 'No connection to internet. Please re-connect.'
|
|
39
38
|
PROPERTY_INVALID = "Invalid property_id - "
|
|
40
39
|
CONFIGURATIONS_FETCH_SUCCESS = "Successfully fetched the configurations."
|
|
41
40
|
RETRY_AFTER_TWO_MINUTES = "Failed to fetch the configurations. Retrying after 2 minutes."
|
|
@@ -19,6 +19,8 @@ import json as json_import
|
|
|
19
19
|
from typing import Optional, Union
|
|
20
20
|
from ibm_cloud_sdk_core import BaseService, DetailedResponse, ApiException
|
|
21
21
|
from requests.exceptions import RetryError
|
|
22
|
+
|
|
23
|
+
from .logger import Logger
|
|
22
24
|
from .url_builder import URLBuilder
|
|
23
25
|
from ibm_appconfiguration.version import __version__
|
|
24
26
|
from ..common import config_constants
|
|
@@ -99,3 +101,26 @@ class APIManager(BaseService):
|
|
|
99
101
|
if isinstance(dictionary, dict):
|
|
100
102
|
return {k: v for (k, v) in dictionary.items() if v is not None}
|
|
101
103
|
return dictionary
|
|
104
|
+
|
|
105
|
+
def get_websocket_headers(self) -> dict:
|
|
106
|
+
"""Get fresh headers for WebSocket connection with current authentication token.
|
|
107
|
+
This method retrieves a fresh authentication token and returns headers
|
|
108
|
+
suitable for WebSocket connections. It should be called each time a
|
|
109
|
+
WebSocket connection is established to ensure the token is valid.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
dict: Headers dictionary containing Authorization and User-Agent
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
Exception: If token retrieval fails, the exception is propagated
|
|
116
|
+
to allow the caller to determine if reconnection should be attempted
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
bearer_token = URLBuilder.get_iam_authenticator().token_manager.get_token()
|
|
120
|
+
return {
|
|
121
|
+
'Authorization': 'Bearer ' + bearer_token,
|
|
122
|
+
'User-Agent': '{0}/{1}'.format(config_constants.SDK_NAME, __version__)
|
|
123
|
+
}
|
|
124
|
+
except Exception as e:
|
|
125
|
+
Logger.error(f"Failed to retrieve IAM token for WebSocket: {str(e)}")
|
|
126
|
+
raise
|
ibm_appconfiguration_python_sdk-0.4.1/ibm_appconfiguration/configurations/internal/utils/socket.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Copyright 2021 IBM All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
This module provides methods to perform operations on the websocket connection to the server.
|
|
17
|
+
"""
|
|
18
|
+
import ssl
|
|
19
|
+
import websocket
|
|
20
|
+
import threading
|
|
21
|
+
from time import sleep
|
|
22
|
+
|
|
23
|
+
from ibm_cloud_sdk_core import ApiException
|
|
24
|
+
|
|
25
|
+
from .logger import Logger
|
|
26
|
+
from ..common import config_constants
|
|
27
|
+
from ..common.config_constants import WEBSOCKET_RECONNECT_DELAY
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Socket:
|
|
31
|
+
"""
|
|
32
|
+
Class to handle the Web socket connection.
|
|
33
|
+
|
|
34
|
+
Example usage:
|
|
35
|
+
# Create socket instance
|
|
36
|
+
socket = Socket()
|
|
37
|
+
|
|
38
|
+
# Setup and connect
|
|
39
|
+
socket.setup(url="wss://example.com", headers={"Authorization": "Bearer token"}, callback=my_callback)
|
|
40
|
+
|
|
41
|
+
# Check connection status
|
|
42
|
+
is_connected = socket.is_connected()
|
|
43
|
+
|
|
44
|
+
# Disconnect and reconnect
|
|
45
|
+
socket.disconnect()
|
|
46
|
+
socket.connect()
|
|
47
|
+
|
|
48
|
+
# Clean up when done
|
|
49
|
+
socket.cancel()
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.__callback = None
|
|
54
|
+
self.ws_client = None
|
|
55
|
+
self.__url = None
|
|
56
|
+
self.__headers_provider = None
|
|
57
|
+
self.__should_reconnect = False
|
|
58
|
+
self.__is_connected = False
|
|
59
|
+
self.__websocket_thread = None
|
|
60
|
+
|
|
61
|
+
def setup(self, url: str, headers_provider, callback) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Setup the socket with connection parameters. If already connected, will disconnect first.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
url: Websocket URL to connect to
|
|
67
|
+
headers_provider: Callable that returns fresh headers dict for the websocket connection
|
|
68
|
+
callback: Callback function for websocket events
|
|
69
|
+
"""
|
|
70
|
+
if not callable(headers_provider):
|
|
71
|
+
Logger.error("headers_provider must be a callable")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
# If already connected with same parameters, do nothing
|
|
75
|
+
if (self.__url == url and
|
|
76
|
+
self.__headers_provider == headers_provider and
|
|
77
|
+
self.__callback == callback and
|
|
78
|
+
self.__is_connected):
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Store new parameters
|
|
82
|
+
self.__url = url
|
|
83
|
+
self.__headers_provider = headers_provider
|
|
84
|
+
self.__callback = callback
|
|
85
|
+
|
|
86
|
+
# Disconnect if already connected
|
|
87
|
+
self.disconnect()
|
|
88
|
+
|
|
89
|
+
# Start new connection
|
|
90
|
+
self.connect()
|
|
91
|
+
|
|
92
|
+
def connect(self) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Explicitly start/restart the websocket connection.
|
|
95
|
+
Safe to call multiple times - will disconnect existing connection first.
|
|
96
|
+
"""
|
|
97
|
+
# Disconnect any existing connection
|
|
98
|
+
self.disconnect()
|
|
99
|
+
|
|
100
|
+
# Start new connection
|
|
101
|
+
self.__should_reconnect = True
|
|
102
|
+
if not self.__websocket_thread or not self.__websocket_thread.is_alive():
|
|
103
|
+
self.__websocket_thread = threading.Thread(target=self.__websocket_run)
|
|
104
|
+
self.__websocket_thread.daemon = True
|
|
105
|
+
self.__websocket_thread.start()
|
|
106
|
+
|
|
107
|
+
def disconnect(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Disconnect the websocket without canceling. Can be reconnected later.
|
|
110
|
+
"""
|
|
111
|
+
self.__should_reconnect = False
|
|
112
|
+
self.__is_connected = False
|
|
113
|
+
if self.ws_client:
|
|
114
|
+
try:
|
|
115
|
+
self.ws_client.close(status=config_constants.CUSTOM_SOCKET_CLOSE_REASON_CODE)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
self.ws_client = None
|
|
119
|
+
|
|
120
|
+
def cancel(self) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Permanently cancel the websocket. Cannot be reconnected after this.
|
|
123
|
+
"""
|
|
124
|
+
self.disconnect()
|
|
125
|
+
self.__url = None
|
|
126
|
+
self.__headers_provider = None
|
|
127
|
+
self.__callback = None
|
|
128
|
+
|
|
129
|
+
def is_connected(self) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Check if websocket is currently connected.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bool: True if connected, False otherwise
|
|
135
|
+
"""
|
|
136
|
+
return self.__is_connected
|
|
137
|
+
|
|
138
|
+
def __websocket_run(self):
|
|
139
|
+
"""Main websocket thread that handles connection and reconnection"""
|
|
140
|
+
while self.__should_reconnect:
|
|
141
|
+
try:
|
|
142
|
+
if not self.__url or not self.__headers_provider:
|
|
143
|
+
Logger.error("URL or headers_provider not configured")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
# Get fresh headers for each connection attempt
|
|
147
|
+
try:
|
|
148
|
+
current_headers = self.__headers_provider()
|
|
149
|
+
if not isinstance(current_headers, dict):
|
|
150
|
+
Logger.error("headers_provider must return a dictionary")
|
|
151
|
+
break
|
|
152
|
+
except ApiException as e:
|
|
153
|
+
Logger.error(f"Error getting headers: {str(e)}")
|
|
154
|
+
# Check if the exception is due to a client error (4xx) from IAM
|
|
155
|
+
if self.__is_token_client_error(e):
|
|
156
|
+
Logger.error("Token retrieval failed with client error (4xx). Stopping WebSocket reconnection.")
|
|
157
|
+
self.__should_reconnect = False
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
# For other errors (5xx, network issues), retry after delay
|
|
161
|
+
if self.__should_reconnect:
|
|
162
|
+
Logger.debug(f"Reconnecting to websocket in {WEBSOCKET_RECONNECT_DELAY} seconds...")
|
|
163
|
+
sleep(WEBSOCKET_RECONNECT_DELAY)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
self.ws_client = websocket.WebSocketApp(
|
|
167
|
+
self.__url,
|
|
168
|
+
on_open=self.on_open,
|
|
169
|
+
on_message=self.on_message,
|
|
170
|
+
on_error=self.on_error,
|
|
171
|
+
on_close=self.on_close,
|
|
172
|
+
header=current_headers
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.ws_client.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
176
|
+
|
|
177
|
+
if self.__should_reconnect:
|
|
178
|
+
Logger.debug(f"Reconnecting to websocket in {WEBSOCKET_RECONNECT_DELAY} seconds...")
|
|
179
|
+
sleep(WEBSOCKET_RECONNECT_DELAY)
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
Logger.error(f"WebSocket error: {str(e)}")
|
|
183
|
+
if self.__should_reconnect:
|
|
184
|
+
Logger.debug(f"Reconnecting to websocket in {WEBSOCKET_RECONNECT_DELAY} seconds...")
|
|
185
|
+
sleep(WEBSOCKET_RECONNECT_DELAY)
|
|
186
|
+
|
|
187
|
+
def __is_token_client_error(self, error) -> bool:
|
|
188
|
+
"""Check if the error from token retrieval is a client-side error (4xx)"""
|
|
189
|
+
# Check for various IBM SDK exception types that might contain status codes
|
|
190
|
+
error_str = str(error).lower()
|
|
191
|
+
|
|
192
|
+
# Common patterns for 4xx errors in IBM SDK exceptions
|
|
193
|
+
if any(code in error_str for code in ['400', '401', '403', '404']):
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Check if exception has a status_code or code attribute
|
|
197
|
+
status_code = getattr(error, 'status_code', None) or getattr(error, 'code', None)
|
|
198
|
+
if status_code is not None:
|
|
199
|
+
try:
|
|
200
|
+
status_code = int(status_code)
|
|
201
|
+
if 400 <= status_code < 500 and status_code != 429 and status_code != 499:
|
|
202
|
+
return True
|
|
203
|
+
except (ValueError, TypeError):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
# Check if it's an ApiException from ibm_cloud_sdk_core
|
|
207
|
+
if hasattr(error, 'message') and hasattr(error, 'http_response'):
|
|
208
|
+
http_response = getattr(error, 'http_response', None)
|
|
209
|
+
if http_response and hasattr(http_response, 'status_code'):
|
|
210
|
+
status_code = http_response.status_code
|
|
211
|
+
if 400 <= status_code < 500 and status_code != 429 and status_code != 499:
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def __is_client_error(self, error) -> bool:
|
|
217
|
+
"""Check if the error is a client-side error (4xx)"""
|
|
218
|
+
if isinstance(error, websocket.WebSocketBadStatusException):
|
|
219
|
+
status_code = getattr(error, 'status_code', None)
|
|
220
|
+
if status_code is not None and 400 <= status_code < 500 and status_code != 429 and status_code != 499:
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def on_message(self, _, message):
|
|
225
|
+
"""Socket on-message callback"""
|
|
226
|
+
if message == 'test message':
|
|
227
|
+
Logger.debug("Received test message from server")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if self.__callback:
|
|
231
|
+
self.__callback(message=message)
|
|
232
|
+
|
|
233
|
+
def on_error(self, _, error):
|
|
234
|
+
"""Socket on-error callback"""
|
|
235
|
+
self.__is_connected = False
|
|
236
|
+
if self.__is_client_error(error):
|
|
237
|
+
# Stop reconnecting on client-side errors
|
|
238
|
+
Logger.error(f"Websocket connect failed due to client error: {error}")
|
|
239
|
+
self.__should_reconnect = False
|
|
240
|
+
else:
|
|
241
|
+
Logger.error(f"Websocket connect failed due to server error: {error}. Reconnecting...")
|
|
242
|
+
|
|
243
|
+
if self.__callback:
|
|
244
|
+
self.__callback(error_state=error)
|
|
245
|
+
|
|
246
|
+
def on_close(self, _, close_status_code, close_msg):
|
|
247
|
+
"""Socket on-close callback"""
|
|
248
|
+
self.__is_connected = False
|
|
249
|
+
if close_status_code is not None and close_status_code == config_constants.CUSTOM_SOCKET_CLOSE_REASON_CODE:
|
|
250
|
+
self.__should_reconnect = False
|
|
251
|
+
|
|
252
|
+
Logger.error(f"Websocket closed with code: {close_status_code} and message: {close_msg}. Reconnecting...")
|
|
253
|
+
if self.__callback:
|
|
254
|
+
self.__callback(closed_state='Closed the web_socket')
|
|
255
|
+
|
|
256
|
+
def on_open(self, _):
|
|
257
|
+
"""Socket on-open callback"""
|
|
258
|
+
self.__is_connected = True
|
|
259
|
+
|
|
260
|
+
if self.__callback:
|
|
261
|
+
self.__callback(open_state='Opened the web_socket')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ibm-appconfiguration-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: IBM Cloud App Configuration Python SDK
|
|
5
5
|
Home-page: https://github.com/IBM/appconfiguration-python-sdk
|
|
6
6
|
Author: IBM
|
|
@@ -357,14 +357,6 @@ def configuration_update(self):
|
|
|
357
357
|
appconfig_client.register_configuration_update_listener(configuration_update)
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
## Fetch latest data
|
|
361
|
-
|
|
362
|
-
Fetch the latest configuration data.
|
|
363
|
-
|
|
364
|
-
```py
|
|
365
|
-
appconfig_client.fetch_configurations()
|
|
366
|
-
```
|
|
367
|
-
|
|
368
360
|
## Enable debugger (Optional)
|
|
369
361
|
|
|
370
362
|
Use this method to enable/disable the logging in SDK.
|
|
@@ -29,11 +29,14 @@ class MyTestCase(unittest.TestCase):
|
|
|
29
29
|
self.expected_closed_state = closed_state
|
|
30
30
|
self.expected_open_state = open_state
|
|
31
31
|
|
|
32
|
+
def headers_provider(self):
|
|
33
|
+
return {}
|
|
34
|
+
|
|
32
35
|
def test_socket(self):
|
|
33
36
|
self.__socket = Socket()
|
|
34
37
|
self.__socket.setup(
|
|
35
38
|
url="ws://testurl.com",
|
|
36
|
-
|
|
39
|
+
headers_provider=self.headers_provider,
|
|
37
40
|
callback=self.callback
|
|
38
41
|
)
|
|
39
42
|
|
|
@@ -51,10 +51,6 @@ class MyTestCase(unittest.TestCase):
|
|
|
51
51
|
sut1.set_context("", "")
|
|
52
52
|
self.assertIsNotNone(sut1.get_apikey())
|
|
53
53
|
|
|
54
|
-
def test_configuration_fetch_feature_data(self):
|
|
55
|
-
sut1 = AppConfiguration.get_instance()
|
|
56
|
-
sut1.fetch_configurations()
|
|
57
|
-
|
|
58
54
|
def response(self):
|
|
59
55
|
print('Get your Feature value NOW')
|
|
60
56
|
|
ibm_appconfiguration_python_sdk-0.3.9/ibm_appconfiguration/configurations/internal/utils/socket.py
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Copyright 2021 IBM All Rights Reserved.
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
# you may not use this file except in compliance with the License.
|
|
5
|
-
# You may obtain a copy of the License at
|
|
6
|
-
#
|
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
# See the License for the specific language governing permissions and
|
|
13
|
-
# limitations under the License.
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
This module provides methods to perform operations on the websocket connection to the server.
|
|
17
|
-
"""
|
|
18
|
-
import ssl
|
|
19
|
-
import websocket
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Socket:
|
|
23
|
-
"""Class to handle the Web socket"""
|
|
24
|
-
def __init__(self):
|
|
25
|
-
self.__callback = None
|
|
26
|
-
self.ws_client = None
|
|
27
|
-
|
|
28
|
-
def setup(self, url, headers, callback):
|
|
29
|
-
""" Setup the socket.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
url: Url for the socket
|
|
33
|
-
headers: Headers for the socket.
|
|
34
|
-
callback: Callback for the socket.
|
|
35
|
-
"""
|
|
36
|
-
self.__callback = callback
|
|
37
|
-
self.ws_client = websocket.WebSocketApp(
|
|
38
|
-
url,
|
|
39
|
-
on_open=self.on_open,
|
|
40
|
-
on_message=self.on_message,
|
|
41
|
-
on_error=self.on_error,
|
|
42
|
-
on_close=self.on_close,
|
|
43
|
-
header=headers
|
|
44
|
-
)
|
|
45
|
-
self.ws_client.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
|
46
|
-
|
|
47
|
-
def on_message(self, _, message):
|
|
48
|
-
"""Socket on-message
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
message: Message object from the socket
|
|
52
|
-
"""
|
|
53
|
-
if message == 'test message':
|
|
54
|
-
return
|
|
55
|
-
self.__callback(message=message)
|
|
56
|
-
|
|
57
|
-
def on_error(self, _, error):
|
|
58
|
-
"""Socket on-error
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
error: Error object from the socket
|
|
62
|
-
"""
|
|
63
|
-
self.__callback(error_state=error)
|
|
64
|
-
self.ws_client.close()
|
|
65
|
-
|
|
66
|
-
def on_close(self, _, close_status_code, close_msg):
|
|
67
|
-
"""Socket on-close call"""
|
|
68
|
-
self.__callback(closed_state='Closed the web_socket')
|
|
69
|
-
|
|
70
|
-
def on_open(self, _):
|
|
71
|
-
"""Socket on-open call"""
|
|
72
|
-
self.__callback(open_state='Opened the web_socket')
|
|
73
|
-
|
|
74
|
-
def cancel(self):
|
|
75
|
-
"""
|
|
76
|
-
Socket cancel.
|
|
77
|
-
"""
|
|
78
|
-
self.ws_client.close()
|
|
File without changes
|
{ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|