ibm-appconfiguration-python-sdk 0.3.8__tar.gz → 0.4.0__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.8/ibm_appconfiguration_python_sdk.egg-info → ibm_appconfiguration_python_sdk-0.4.0}/PKG-INFO +1 -9
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/README.md +0 -8
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/appconfiguration.py +1 -17
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/configuration_handler.py +26 -76
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/config_constants.py +2 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/config_messages.py +0 -1
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/api_manager.py +25 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/file_manager.py +7 -8
- ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration/configurations/internal/utils/parser.py +149 -0
- ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration/configurations/internal/utils/socket.py +261 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/url_builder.py +2 -2
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/version.py +1 -1
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration_python_sdk.egg-info}/PKG-INFO +1 -9
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/SOURCES.txt +1 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/setup.py +1 -1
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/test_configuration_handler.py +87 -68
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_api_manager.py +14 -3
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_file_manager.py +2 -2
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_socket.py +4 -1
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_url_builder.py +4 -4
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/test_appconfiguration.py +1 -5
- ibm_appconfiguration_python_sdk-0.3.8/ibm_appconfiguration/configurations/internal/utils/socket.py +0 -78
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/LICENSE +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/sample_app.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/server_sample.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/compute_percentage.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/connectivity.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/logger.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/metering.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/validators.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/configuration_type.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/feature.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/property.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/rule.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/segment.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/segment_rules.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/dependency_links.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/requires.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/top_level.txt +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/integration_tests/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/integration_tests/test_integration.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/setup.cfg +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_feature.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_property.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_rule.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_segment.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_segment_rules.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/__init__.py +0 -0
- {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_metering.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.0
|
|
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
|
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
"""
|
|
16
16
|
Internal class to handle the configuration.
|
|
17
17
|
"""
|
|
18
|
+
import json
|
|
18
19
|
import os
|
|
19
20
|
from typing import Dict, List, Any
|
|
20
21
|
from threading import Timer, Thread
|
|
21
22
|
from ibm_appconfiguration.configurations.internal.common import config_messages, config_constants
|
|
22
23
|
from .internal.utils.logger import Logger
|
|
24
|
+
from .internal.utils.parser import extract_configurations, format_config
|
|
23
25
|
from .internal.utils.validators import Validators
|
|
24
26
|
from .models import Feature
|
|
25
27
|
from .models import SegmentRules
|
|
@@ -30,25 +32,13 @@ from .internal.utils.compute_percentage import get_normalized_value
|
|
|
30
32
|
from .internal.utils.metering import Metering
|
|
31
33
|
from .internal.utils.socket import Socket
|
|
32
34
|
from .internal.utils.url_builder import URLBuilder
|
|
33
|
-
from .internal.utils.connectivity import Connectivity
|
|
34
35
|
from .internal.utils.api_manager import APIManager
|
|
35
|
-
import sys
|
|
36
|
-
from time import sleep
|
|
37
|
-
|
|
38
|
-
# Server max time out is assumed to be 1 week = 604800 seconds = 40320*15
|
|
39
|
-
sys.setrecursionlimit(40320)
|
|
40
|
-
|
|
41
|
-
# delay between each web socket connection retry
|
|
42
|
-
delay = 15
|
|
43
36
|
|
|
44
37
|
|
|
45
38
|
class ConfigurationHandler:
|
|
46
39
|
"""Internal class to handle the configuration"""
|
|
47
40
|
__instance = None
|
|
48
41
|
|
|
49
|
-
# variable to keep track of server-client connection status
|
|
50
|
-
__is_alive = False
|
|
51
|
-
|
|
52
42
|
@staticmethod
|
|
53
43
|
def get_instance():
|
|
54
44
|
""" Static access method. """
|
|
@@ -80,8 +70,6 @@ class ConfigurationHandler:
|
|
|
80
70
|
self.__on_socket_retry = False
|
|
81
71
|
self.__override_service_url = None
|
|
82
72
|
self.__socket = None
|
|
83
|
-
self.__connectivity = None
|
|
84
|
-
self.__is_network_connected = True
|
|
85
73
|
self.__api_manager = None
|
|
86
74
|
self.__use_private_endpoint = False
|
|
87
75
|
|
|
@@ -136,7 +124,6 @@ class ConfigurationHandler:
|
|
|
136
124
|
self.__bootstrap_file = options['bootstrap_file']
|
|
137
125
|
self.__persistent_cache_dir = options['persistent_cache_dir']
|
|
138
126
|
self.__is_initialized = True
|
|
139
|
-
self.__check_network()
|
|
140
127
|
|
|
141
128
|
def load_data(self):
|
|
142
129
|
"""Load the configuration data"""
|
|
@@ -147,7 +134,9 @@ class ConfigurationHandler:
|
|
|
147
134
|
self.__persistent_data = FileManager.read_files(
|
|
148
135
|
file_path=os.path.join(self.__persistent_cache_dir, 'appconfiguration.json'))
|
|
149
136
|
if self.__persistent_data is not None:
|
|
150
|
-
self.__load_configurations(
|
|
137
|
+
self.__load_configurations(
|
|
138
|
+
extract_configurations(self.__persistent_data, self.__environment_id, self.__collection_id)
|
|
139
|
+
)
|
|
151
140
|
if not os.access(self.__persistent_cache_dir, os.W_OK):
|
|
152
141
|
Logger.error(config_messages.ERROR_NO_WRITE_PERMISSION)
|
|
153
142
|
return
|
|
@@ -156,11 +145,13 @@ class ConfigurationHandler:
|
|
|
156
145
|
if self.__persistent_data is None or len(self.__persistent_data) == 0:
|
|
157
146
|
bootstrap_file_data = FileManager.read_files(file_path=self.__bootstrap_file)
|
|
158
147
|
if bootstrap_file_data is not None:
|
|
159
|
-
self.
|
|
148
|
+
configurations = extract_configurations(bootstrap_file_data, self.__environment_id, self.__collection_id)
|
|
149
|
+
self.__load_configurations(configurations)
|
|
150
|
+
self.__write_to_persistent_storage(format_config(configurations, self.__environment_id, self.__collection_id),
|
|
151
|
+
self.__persistent_cache_dir)
|
|
160
152
|
else:
|
|
161
153
|
Logger.error("Error reading bootstrap file data")
|
|
162
154
|
return
|
|
163
|
-
self.__write_to_persistent_storage(bootstrap_file_data, self.__persistent_cache_dir)
|
|
164
155
|
if self.__configuration_update_listener and callable(self.__configuration_update_listener):
|
|
165
156
|
self.__configuration_update_listener()
|
|
166
157
|
else:
|
|
@@ -169,7 +160,9 @@ class ConfigurationHandler:
|
|
|
169
160
|
else:
|
|
170
161
|
bootstrap_file_data = FileManager.read_files(file_path=self.__bootstrap_file)
|
|
171
162
|
if bootstrap_file_data is not None:
|
|
172
|
-
self.__load_configurations(
|
|
163
|
+
self.__load_configurations(
|
|
164
|
+
extract_configurations(bootstrap_file_data, self.__environment_id, self.__collection_id)
|
|
165
|
+
)
|
|
173
166
|
else:
|
|
174
167
|
Logger.error("Error reading bootstrap file data")
|
|
175
168
|
return
|
|
@@ -193,28 +186,6 @@ class ConfigurationHandler:
|
|
|
193
186
|
else:
|
|
194
187
|
Logger.error(config_messages.CONFIGURATION_HANDLER_METHOD_ERROR)
|
|
195
188
|
|
|
196
|
-
def __check_network(self):
|
|
197
|
-
if self.__live_config_update_enabled:
|
|
198
|
-
if self.__connectivity is None:
|
|
199
|
-
self.__connectivity = Connectivity.get_instance()
|
|
200
|
-
self.__connectivity.add_connectivity_listener(self.__network_listener)
|
|
201
|
-
self.__connectivity.check_connection()
|
|
202
|
-
else:
|
|
203
|
-
self.__connectivity = None
|
|
204
|
-
|
|
205
|
-
def __network_listener(self, is_connected: bool):
|
|
206
|
-
if not self.__live_config_update_enabled:
|
|
207
|
-
self.__connectivity = None
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
if is_connected:
|
|
211
|
-
if not self.__is_network_connected:
|
|
212
|
-
self.__is_network_connected = True
|
|
213
|
-
self.__fetch_config_data()
|
|
214
|
-
else:
|
|
215
|
-
Logger.debug(config_messages.NO_INTERNET_CONNECTION_ERROR)
|
|
216
|
-
self.__is_network_connected = False
|
|
217
|
-
|
|
218
189
|
def get_properties(self) -> Dict[str, Property]:
|
|
219
190
|
"""Get the list of Property objects
|
|
220
191
|
|
|
@@ -263,24 +234,15 @@ class ConfigurationHandler:
|
|
|
263
234
|
if self.__is_initialized:
|
|
264
235
|
self.__fetch_from_api()
|
|
265
236
|
self.__on_socket_retry = False
|
|
266
|
-
|
|
267
|
-
config_thread = Thread(target=self.__start_web_socket, args=())
|
|
268
|
-
config_thread.daemon = True
|
|
269
|
-
config_thread.start()
|
|
237
|
+
self.__start_web_socket()
|
|
270
238
|
|
|
271
239
|
def __start_web_socket(self):
|
|
272
|
-
|
|
273
|
-
headers = {
|
|
274
|
-
'Authorization': 'Bearer ' + bearer_token
|
|
275
|
-
}
|
|
276
|
-
if self.__socket:
|
|
277
|
-
self.__socket.cancel()
|
|
278
|
-
self.__socket = None
|
|
240
|
+
|
|
279
241
|
self.__socket = Socket()
|
|
280
242
|
self.__socket.setup(
|
|
281
243
|
url=URLBuilder.get_web_socket_url(),
|
|
282
|
-
|
|
283
|
-
callback=self.
|
|
244
|
+
headers_provider=self.__api_manager.get_websocket_headers,
|
|
245
|
+
callback=self.__handle_socket_events
|
|
284
246
|
)
|
|
285
247
|
|
|
286
248
|
def __load_configurations(self, data: dict):
|
|
@@ -478,8 +440,8 @@ class ConfigurationHandler:
|
|
|
478
440
|
Logger.debug(err)
|
|
479
441
|
return rule_map
|
|
480
442
|
|
|
481
|
-
def __write_to_persistent_storage(self,
|
|
482
|
-
FileManager.store_files(json, os.path.join(file_path, 'appconfiguration.json'))
|
|
443
|
+
def __write_to_persistent_storage(self, data: str, file_path: str):
|
|
444
|
+
FileManager.store_files(json.dumps(json.loads(data), indent=2), os.path.join(file_path, 'appconfiguration.json'))
|
|
483
445
|
|
|
484
446
|
def __fetch_from_api(self):
|
|
485
447
|
if self.__is_initialized:
|
|
@@ -506,8 +468,8 @@ class ConfigurationHandler:
|
|
|
506
468
|
Logger.info(config_messages.CONFIGURATIONS_FETCH_SUCCESS)
|
|
507
469
|
response_data = response.get_result()
|
|
508
470
|
try:
|
|
509
|
-
|
|
510
|
-
self.__load_configurations(
|
|
471
|
+
configurations = extract_configurations(json.dumps(response_data), self.__environment_id, self.__collection_id)
|
|
472
|
+
self.__load_configurations(configurations) # load response to cache maps
|
|
511
473
|
if self.__configuration_update_listener and callable(self.__configuration_update_listener):
|
|
512
474
|
self.__configuration_update_listener()
|
|
513
475
|
# we have already loaded the configurations to feature & property dicts.
|
|
@@ -515,7 +477,7 @@ class ConfigurationHandler:
|
|
|
515
477
|
# But the thread shouldn't be a daemon thread, because the writing should complete even if the main thread has terminated.
|
|
516
478
|
if self.__persistent_cache_dir:
|
|
517
479
|
file_write_thread = Thread(target=self.__write_to_persistent_storage,
|
|
518
|
-
|
|
480
|
+
args=(format_config(configurations, self.__environment_id, self.__collection_id), self.__persistent_cache_dir))
|
|
519
481
|
file_write_thread.start()
|
|
520
482
|
except Exception as exception:
|
|
521
483
|
Logger.error(f'error while while fetching {exception}')
|
|
@@ -538,32 +500,20 @@ class ConfigurationHandler:
|
|
|
538
500
|
else:
|
|
539
501
|
Logger.debug(config_messages.CONFIGURATION_HANDLER_INIT_ERROR)
|
|
540
502
|
|
|
541
|
-
def
|
|
542
|
-
|
|
503
|
+
def __handle_socket_events(self, message=None, error_state=None,
|
|
504
|
+
closed_state=None, open_state=None):
|
|
543
505
|
if message:
|
|
544
|
-
|
|
506
|
+
Logger.debug(f'Received message from websocket. {message}')
|
|
545
507
|
self.__fetch_from_api()
|
|
546
|
-
Logger.debug(f'Received message from socket. {message}')
|
|
547
508
|
elif error_state:
|
|
548
|
-
self.__is_alive = False
|
|
549
|
-
Logger.error(f'Received error from socket. {error_state}')
|
|
550
|
-
Logger.info('Reconnecting to server....')
|
|
551
509
|
self.__on_socket_retry = True
|
|
552
|
-
sleep(delay)
|
|
553
|
-
self.__start_web_socket()
|
|
554
510
|
elif closed_state:
|
|
555
|
-
self.__is_alive = False
|
|
556
|
-
Logger.error('Received close connection from socket.')
|
|
557
|
-
Logger.info('Reconnecting to server....')
|
|
558
511
|
self.__on_socket_retry = True
|
|
559
|
-
sleep(delay)
|
|
560
|
-
self.__start_web_socket()
|
|
561
512
|
elif open_state:
|
|
562
|
-
|
|
513
|
+
Logger.debug('Received opened connection from websocket.')
|
|
563
514
|
if self.__on_socket_retry:
|
|
564
515
|
self.__on_socket_retry = False
|
|
565
516
|
self.__fetch_from_api()
|
|
566
|
-
Logger.debug('Received opened connection from socket.')
|
|
567
517
|
else:
|
|
568
518
|
Logger.error('Unknown Error inside the socket connection.')
|
|
569
519
|
|
|
@@ -572,4 +522,4 @@ class ConfigurationHandler:
|
|
|
572
522
|
|
|
573
523
|
Returns: boolean indicating connection status
|
|
574
524
|
"""
|
|
575
|
-
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
|
|
@@ -18,8 +18,7 @@ file based cache of the SDK.
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import fcntl
|
|
21
|
-
import
|
|
22
|
-
from typing import Optional, Any
|
|
21
|
+
from typing import Optional
|
|
23
22
|
|
|
24
23
|
from .logger import Logger
|
|
25
24
|
|
|
@@ -28,17 +27,17 @@ class FileManager:
|
|
|
28
27
|
"""FileManager to handle the cache"""
|
|
29
28
|
|
|
30
29
|
@classmethod
|
|
31
|
-
def store_files(cls,
|
|
30
|
+
def store_files(cls, data: str, file_path: str) -> bool:
|
|
32
31
|
"""Store the file
|
|
33
32
|
|
|
34
33
|
Args:
|
|
35
|
-
|
|
34
|
+
data: Data to be stored.
|
|
36
35
|
file_path: File path for the cache.
|
|
37
36
|
"""
|
|
38
37
|
try:
|
|
39
38
|
with open(file_path, 'w') as cache:
|
|
40
39
|
fcntl.flock(cache, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
41
|
-
|
|
40
|
+
cache.write(data)
|
|
42
41
|
fcntl.flock(cache, fcntl.LOCK_UN)
|
|
43
42
|
return True
|
|
44
43
|
except Exception as err:
|
|
@@ -46,7 +45,7 @@ class FileManager:
|
|
|
46
45
|
return False
|
|
47
46
|
|
|
48
47
|
@classmethod
|
|
49
|
-
def read_files(cls, file_path: str) -> Optional[
|
|
48
|
+
def read_files(cls, file_path: str) -> Optional[str]:
|
|
50
49
|
"""
|
|
51
50
|
Read the data from the given path.
|
|
52
51
|
|
|
@@ -59,9 +58,9 @@ class FileManager:
|
|
|
59
58
|
try:
|
|
60
59
|
with open(file_path, 'r') as file:
|
|
61
60
|
fcntl.flock(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
62
|
-
data =
|
|
61
|
+
data = file.read()
|
|
63
62
|
fcntl.flock(file, fcntl.LOCK_UN)
|
|
64
|
-
return data
|
|
63
|
+
return data if len(data) > 0 else None
|
|
65
64
|
except Exception as err:
|
|
66
65
|
Logger.error(err)
|
|
67
66
|
return None
|
ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration/configurations/internal/utils/parser.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List, Set
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_environment_data(data: Dict[str, Any], environment_id: str) -> Dict[str, Any]:
|
|
6
|
+
"""
|
|
7
|
+
Prepares config data for extraction with validation.
|
|
8
|
+
|
|
9
|
+
:param data: The full JSON configuration as a dictionary.
|
|
10
|
+
:param environment_id: The environment ID to extract.
|
|
11
|
+
:return: A dictionary containing 'features', 'properties', and 'segments'.
|
|
12
|
+
:raises: Exception if the format is invalid or environment not found.
|
|
13
|
+
"""
|
|
14
|
+
if not isinstance(data.get("segments"), list) or not isinstance(data.get("environments"), list):
|
|
15
|
+
raise Exception("Improper Data format present in configuration")
|
|
16
|
+
|
|
17
|
+
for environment in data["environments"]:
|
|
18
|
+
if environment.get("environment_id") == environment_id:
|
|
19
|
+
return {
|
|
20
|
+
"features": environment.get("features", []),
|
|
21
|
+
"properties": environment.get("properties", []),
|
|
22
|
+
"segments": data["segments"]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
raise Exception("Matching environment not found in configuration")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_resource(resource: Dict[str, Any], collection: str) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Validates if the feature/property belongs to the given collection.
|
|
31
|
+
|
|
32
|
+
:param resource: The feature or property dictionary.
|
|
33
|
+
:param collection: The collection ID to match.
|
|
34
|
+
:return: True if valid, False otherwise.
|
|
35
|
+
:raises: Exception if collection format is invalid.
|
|
36
|
+
"""
|
|
37
|
+
if "collections" not in resource:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
collections = resource["collections"]
|
|
41
|
+
if not isinstance(collections, list):
|
|
42
|
+
raise Exception("Improper collection format in resource data")
|
|
43
|
+
|
|
44
|
+
for col in collections:
|
|
45
|
+
if col.get("collection_id") == collection:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def append_segment_ids(resource: Dict[str, Any], segment_ids: Set[str]):
|
|
52
|
+
"""
|
|
53
|
+
Appends segment IDs from the resource's segment rules into the given set.
|
|
54
|
+
|
|
55
|
+
:param resource: The feature or property dictionary.
|
|
56
|
+
:param segment_ids: A set to accumulate segment IDs.
|
|
57
|
+
"""
|
|
58
|
+
for segment_rule in resource.get("segment_rules", []):
|
|
59
|
+
for rule in segment_rule.get("rules", []):
|
|
60
|
+
for segment_id in rule.get("segments", []):
|
|
61
|
+
segment_ids.add(segment_id)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract_resources(resource_data: Dict[str, Any], collection: str) -> Dict[str, List[Any]]:
|
|
65
|
+
"""
|
|
66
|
+
Extracts features, properties, and segments after validation.
|
|
67
|
+
|
|
68
|
+
:param resource_data: The environment-specific data.
|
|
69
|
+
:param collection: The collection ID to validate against.
|
|
70
|
+
:return: A dictionary with keys 'features', 'properties', and 'segments'.
|
|
71
|
+
:raises: Exception if any required segment is missing.
|
|
72
|
+
"""
|
|
73
|
+
features = []
|
|
74
|
+
properties = []
|
|
75
|
+
segments = []
|
|
76
|
+
required_segment_ids = set()
|
|
77
|
+
|
|
78
|
+
for feature in resource_data.get("features", []):
|
|
79
|
+
if validate_resource(feature, collection):
|
|
80
|
+
append_segment_ids(feature, required_segment_ids)
|
|
81
|
+
features.append(feature)
|
|
82
|
+
|
|
83
|
+
for property_ in resource_data.get("properties", []):
|
|
84
|
+
if validate_resource(property_, collection):
|
|
85
|
+
append_segment_ids(property_, required_segment_ids)
|
|
86
|
+
properties.append(property_)
|
|
87
|
+
|
|
88
|
+
available_segments = resource_data.get("segments", [])
|
|
89
|
+
for segment in available_segments:
|
|
90
|
+
if segment.get("segment_id") in required_segment_ids:
|
|
91
|
+
segments.append(segment)
|
|
92
|
+
required_segment_ids.remove(segment.get("segment_id"))
|
|
93
|
+
|
|
94
|
+
if len(required_segment_ids) > 0:
|
|
95
|
+
raise Exception(f"Required segment doesn't exist in provided segments")
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"features": features,
|
|
99
|
+
"properties": properties,
|
|
100
|
+
"segments": segments
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_configurations(data: str, environment: str, collection: str) -> Dict[str, List[Any]]:
|
|
105
|
+
"""
|
|
106
|
+
Unified parser for app-config data for new SDK/export/promote format.
|
|
107
|
+
|
|
108
|
+
:param data: Raw JSON string of the config.
|
|
109
|
+
:param environment: The environment ID.
|
|
110
|
+
:param collection: The collection ID.
|
|
111
|
+
:return: A dictionary with 'features', 'properties', and 'segments'.
|
|
112
|
+
:raises: Exception on any validation or format error.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
configurations = json.loads(data)
|
|
116
|
+
|
|
117
|
+
if "collections" not in configurations or not isinstance(configurations["collections"], list):
|
|
118
|
+
raise Exception("Improper/Missing collections in configuration")
|
|
119
|
+
|
|
120
|
+
if not any(col.get("collection_id") == collection for col in configurations["collections"]):
|
|
121
|
+
raise Exception("Required collection not found in collections")
|
|
122
|
+
|
|
123
|
+
config_data = extract_environment_data(configurations, environment)
|
|
124
|
+
return extract_resources(config_data, collection)
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise Exception(f"Extraction of configurations failed with error:\n {str(e)}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_config(res: Dict[str, List[Any]], environment_id: str, collection_id: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Formats the extracted resources into unified config format.
|
|
133
|
+
|
|
134
|
+
:param res: The extracted config (from `extract_configurations`).
|
|
135
|
+
:param environment_id: The environment ID to include.
|
|
136
|
+
:param collection_id: The collection ID to include.
|
|
137
|
+
:return: A formatted configuration dictionary.
|
|
138
|
+
"""
|
|
139
|
+
return json.dumps({
|
|
140
|
+
"environments": [
|
|
141
|
+
{
|
|
142
|
+
"environment_id": environment_id,
|
|
143
|
+
"features": res.get("features", []),
|
|
144
|
+
"properties": res.get("properties", [])
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"collections": [{"collection_id": collection_id}],
|
|
148
|
+
"segments": res.get("segments", [])
|
|
149
|
+
})
|