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.
Files changed (59) hide show
  1. {ibm_appconfiguration_python_sdk-0.3.8/ibm_appconfiguration_python_sdk.egg-info → ibm_appconfiguration_python_sdk-0.4.0}/PKG-INFO +1 -9
  2. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/README.md +0 -8
  3. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/appconfiguration.py +1 -17
  4. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/configuration_handler.py +26 -76
  5. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/config_constants.py +2 -0
  6. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/config_messages.py +0 -1
  7. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/api_manager.py +25 -0
  8. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/file_manager.py +7 -8
  9. ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration/configurations/internal/utils/parser.py +149 -0
  10. ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration/configurations/internal/utils/socket.py +261 -0
  11. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/url_builder.py +2 -2
  12. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/version.py +1 -1
  13. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0/ibm_appconfiguration_python_sdk.egg-info}/PKG-INFO +1 -9
  14. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/SOURCES.txt +1 -0
  15. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/setup.py +1 -1
  16. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/test_configuration_handler.py +87 -68
  17. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_api_manager.py +14 -3
  18. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_file_manager.py +2 -2
  19. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_socket.py +4 -1
  20. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/test_url_builder.py +4 -4
  21. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/test_appconfiguration.py +1 -5
  22. ibm_appconfiguration_python_sdk-0.3.8/ibm_appconfiguration/configurations/internal/utils/socket.py +0 -78
  23. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/LICENSE +0 -0
  24. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/__init__.py +0 -0
  25. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/sample_app.py +0 -0
  26. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/examples/server_sample.py +0 -0
  27. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/__init__.py +0 -0
  28. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/__init__.py +0 -0
  29. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/__init__.py +0 -0
  30. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/common/__init__.py +0 -0
  31. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/__init__.py +0 -0
  32. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/compute_percentage.py +0 -0
  33. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/connectivity.py +0 -0
  34. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/logger.py +0 -0
  35. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/metering.py +0 -0
  36. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/internal/utils/validators.py +0 -0
  37. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/__init__.py +0 -0
  38. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/configuration_type.py +0 -0
  39. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/feature.py +0 -0
  40. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/property.py +0 -0
  41. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/rule.py +0 -0
  42. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/segment.py +0 -0
  43. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration/configurations/models/segment_rules.py +0 -0
  44. {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
  45. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/ibm_appconfiguration_python_sdk.egg-info/requires.txt +0 -0
  46. {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
  47. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/integration_tests/__init__.py +0 -0
  48. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/integration_tests/test_integration.py +0 -0
  49. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/setup.cfg +0 -0
  50. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/__init__.py +0 -0
  51. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/__init__.py +0 -0
  52. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/__init__.py +0 -0
  53. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_feature.py +0 -0
  54. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_property.py +0 -0
  55. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_rule.py +0 -0
  56. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_segment.py +0 -0
  57. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/models/test_segment_rules.py +0 -0
  58. {ibm_appconfiguration_python_sdk-0.3.8 → ibm_appconfiguration_python_sdk-0.4.0}/unit_tests/configurations/utils/__init__.py +0 -0
  59. {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.8
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.__load_data_now()
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(self.__persistent_data)
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.__load_configurations(bootstrap_file_data)
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(bootstrap_file_data)
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
- # Socket connection is a long-running background task, and is safe to run as daemon threads
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
- bearer_token = URLBuilder.get_iam_authenticator().token_manager.get_token()
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
- headers=headers,
283
- callback=self.__on_web_socket_callback
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, json: dict, file_path: str):
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
- response_data = dict(response_data)
510
- self.__load_configurations(response_data) # load response to cache maps
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
- args=(response_data, self.__persistent_cache_dir,))
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 __on_web_socket_callback(self, message=None, error_state=None,
542
- closed_state=None, open_state=None):
503
+ def __handle_socket_events(self, message=None, error_state=None,
504
+ closed_state=None, open_state=None):
543
505
  if message:
544
- self.__is_alive = True
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
- self.__is_alive = True
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.__is_alive
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 json
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, json_data: {}, file_path: str) -> bool:
30
+ def store_files(cls, data: str, file_path: str) -> bool:
32
31
  """Store the file
33
32
 
34
33
  Args:
35
- json_data: Data to be stored.
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
- json.dump(json_data, cache)
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[Any]:
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 = json.load(file)
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
@@ -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
+ })