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.
Files changed (59) hide show
  1. {ibm_appconfiguration_python_sdk-0.3.9/ibm_appconfiguration_python_sdk.egg-info → ibm_appconfiguration_python_sdk-0.4.1}/PKG-INFO +1 -9
  2. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/README.md +0 -8
  3. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/appconfiguration.py +1 -17
  4. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/configuration_handler.py +9 -69
  5. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/config_constants.py +2 -0
  6. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/config_messages.py +0 -1
  7. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/api_manager.py +25 -0
  8. ibm_appconfiguration_python_sdk-0.4.1/ibm_appconfiguration/configurations/internal/utils/socket.py +261 -0
  9. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/configuration_type.py +1 -0
  10. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/version.py +1 -1
  11. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1/ibm_appconfiguration_python_sdk.egg-info}/PKG-INFO +1 -9
  12. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/setup.py +1 -1
  13. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_socket.py +4 -1
  14. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/test_appconfiguration.py +0 -4
  15. ibm_appconfiguration_python_sdk-0.3.9/ibm_appconfiguration/configurations/internal/utils/socket.py +0 -78
  16. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/LICENSE +0 -0
  17. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/__init__.py +0 -0
  18. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/sample_app.py +0 -0
  19. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/examples/server_sample.py +0 -0
  20. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/__init__.py +0 -0
  21. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/__init__.py +0 -0
  22. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/__init__.py +0 -0
  23. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/common/__init__.py +0 -0
  24. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/__init__.py +0 -0
  25. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/compute_percentage.py +0 -0
  26. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/connectivity.py +0 -0
  27. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/file_manager.py +0 -0
  28. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/logger.py +0 -0
  29. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/metering.py +0 -0
  30. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/parser.py +0 -0
  31. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/url_builder.py +0 -0
  32. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/internal/utils/validators.py +0 -0
  33. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/__init__.py +0 -0
  34. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/feature.py +0 -0
  35. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/property.py +0 -0
  36. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/rule.py +0 -0
  37. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/segment.py +0 -0
  38. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration/configurations/models/segment_rules.py +0 -0
  39. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/SOURCES.txt +0 -0
  40. {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
  41. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/ibm_appconfiguration_python_sdk.egg-info/requires.txt +0 -0
  42. {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
  43. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/integration_tests/__init__.py +0 -0
  44. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/integration_tests/test_integration.py +0 -0
  45. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/setup.cfg +0 -0
  46. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/__init__.py +0 -0
  47. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/__init__.py +0 -0
  48. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/__init__.py +0 -0
  49. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_feature.py +0 -0
  50. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_property.py +0 -0
  51. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_rule.py +0 -0
  52. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_segment.py +0 -0
  53. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/models/test_segment_rules.py +0 -0
  54. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/test_configuration_handler.py +0 -0
  55. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/__init__.py +0 -0
  56. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_api_manager.py +0 -0
  57. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_file_manager.py +0 -0
  58. {ibm_appconfiguration_python_sdk-0.3.9 → ibm_appconfiguration_python_sdk-0.4.1}/unit_tests/configurations/utils/test_metering.py +0 -0
  59. {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.9
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.__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
 
@@ -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
- # Socket connection is a long-running background task, and is safe to run as daemon threads
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
- bearer_token = URLBuilder.get_iam_authenticator().token_manager.get_token()
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
- headers=headers,
293
- callback=self.__on_web_socket_callback
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 __on_web_socket_callback(self, message=None, error_state=None,
552
- closed_state=None, open_state=None):
503
+ def __handle_socket_events(self, message=None, error_state=None,
504
+ closed_state=None, open_state=None):
553
505
  if message:
554
- self.__is_alive = True
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
- self.__is_alive = True
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.__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
@@ -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')
@@ -24,3 +24,4 @@ class ConfigurationType(enum.Enum):
24
24
  NUMERIC = 'NUMERIC'
25
25
  STRING = 'STRING'
26
26
  BOOLEAN = 'BOOLEAN'
27
+ SECRETREF = 'SECRETREF'
@@ -15,4 +15,4 @@
15
15
  """
16
16
  Version of ibm-appconfiguration-python-sdk
17
17
  """
18
- __version__ = '0.3.9'
18
+ __version__ = '0.4.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ibm-appconfiguration-python-sdk
3
- Version: 0.3.9
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.
@@ -12,7 +12,7 @@
12
12
  from setuptools import setup, find_packages
13
13
 
14
14
  NAME = "ibm-appconfiguration-python-sdk"
15
- VERSION = "0.3.9"
15
+ VERSION = "0.4.1"
16
16
  # To install the library, run the following
17
17
  #
18
18
  # python setup.py install
@@ -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
- headers=[],
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
 
@@ -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()